Houston ??????

Le module signals contient une implémentation du design pattern observer, c’est à dire un moyen de lier un callback à un événement afin de pouvoir y réagir quand il se déclenche.

Les événements peuvent être aussi divers que “une requête a commencé” ou “un modèle a été sauvé en base de données”.

L’utilisation d’un signal se fait en 3 temps:

  1. décider à quel événement on veut réagir;
  2. décider ce qu’on veut faire quand il se déclenche;
  3. associer l’événement à l’action.

Par exemple, après la suppression d’un modèle, je veux faire un travail de nettoyage dans ma base de données.

Je choisis l’événement auquel je veux réagir dans la liste des signaux disponibles:

from django.db.models.signals import post_delete

Ici j’ai choisi post_delete, car c’est un signal qui est envoyé après la suppression d’un modèle automatiquement par Django.

Ensuite je décide de ce que je veux faire quand l’action se déclenche en écrivant une fonction:

def reaction_au_signal(sender, **kwargs):
    # faire ici mon opération de nettoyage 

Vous noterez les arguments très spécifiques que j’accepte. Ils sont propres à l’implémentation des signaux de Django et cette signature est obligatoire. sender est l’objet qui envoie le signal. Dans notre cas ce sera la classe du modèle supprimé. kwargs c’est tout le reste, et son contenu dépend du type de signal reçu. Généralement il y a plein d’informations dedans sur le contexte qui nous permettent de prendre des décisions.

Ainsi, pour le signal post_delete, kwargs contient l’instance du modèle supprimé (dont les valeurs ne sont donc plus en base de données, ne faites pas de query avec !) et le nom de la base de données utilisés si vous en utilisez plusieurs.

Enfin il faut associer l’événement à l’action, en utilisant la fonction connect().

post_delete.connect(reaction_au_signal)

Le code complet donne ceci:

from django.db.models.signals import post_delete

def reaction_au_signal(sender, **kwargs):
    # faire ici mon opération de nettoyage dans la base de donnée

post_delete.connect(reaction_au_signal)

Réponses à quelques questions existentielles

Mais pourquoi que à quoi ça sert de quoi donc ?

Pourquoi ne pas utiliser plutôt l’héritage, ou mettre des hooks de callback ? Et bien tout simplement parce que les signaux permettent de réagir à une événement sans avoir à toucher le code qui génère l’événement. C’est un moyen très flexible et puissant de réagir à ce que fait du code dans une autre application. Ou de permettre à du code d’autres applications de réagir à vos événements sans avoir à toucher à votre code. Car oui, vous pouvez définir vos propres signaux.

Je les mets où ces signaux ?

Généralement dans le module qui contient la même sémantique que le signal: si c’est pour les requêtes HTTP, dans views.py, si c’est pour l’ORM, dans models.py, etc.

Est-ce que Dieu existe ?

Cher lecteur, merci de ne pas prononcer mon nom en vain.

Quelques astuces avec les signaux

Il est rare que vous vouliez réagir à la suppression de tous les modèles, ou de toutes les requêtes. Généralement on veut uniquement réagir à un signal pour un émetteur précis. Django permet cela en vous laissant choisir le sender:


class TopModel(models.Model):
    bonnet = models.TextField(max_char=1, default="A")

post_delete.connect(reaction_au_signal, sender=TopModel)

Ainsi, reaction_au_signal() ne sera appelée qu’à la suppression d’un objet de type TopModel.

Sachez aussi qu’il existe une syntaxe à base de décorateurs. Ca fait la même chose, mais perso je préfère la style:

from django.db.models.signals import post_delete
from django.dispatch import receiver

@receiver(pre_save, sender=MyModel)
def reaction_au_signal(sender, **kwargs):
    ...

Il faut aussi savoir que parfois, certains modules sont exécutés plusieurs fois, et si le handler du signal est déclaré dedans, il va être attaché plusieurs fois au signal, et donc lancé plusieurs fois quand le signal se déclenche. Pas glop.

Pour éviter ça, on peut passer un identifiant unique en attachant le signal:

post_delete.connect(reaction_au_signal, dispatch_uid="une_chaine_de_caracteres_quelconque")

dispatch_uid peut contenir n’importe quoi, pourvu qu’il soit unique à cette association signal/callback.

Il y a aussi un autre paramètre est nommé weak. C’est un booléen par défaut mis sur True qui décide si la référence vers le handler est une référence faible ou non. Mettez le sur False si votre callback n’a aucune autre référence que le signal, par exemple si la fonction est générée à la volée, ou si c’est une lambda.

Enfin, les signaux ne sont PAS exécutés dans un thread à part, donc ils bloquent l’exécution du programme comme le reste du code. La bonne nouvelle, c’est que ça vous permet d’utiliser pdb dedans. C’est particulièrement utile pour les signaux complexes comme m2m_changed qui est assez compliqué et pour lequel on s’y reprend à plusieurs fois.

Ah, juste un dernier détails. Model.objects.update() ne déclenche aucun signal…

m2m_changed, ce petit bâtard

La plupart des signaux sont super faciles à manipuler. Sauf un. m2m_changed. C’est un enculé.

Il faut savoir que quand vous utilisez models.ManyToManyField, une troisième table est automatiquement et silencieusement créé qui contient l’association entre vos deux modèles, avec le modèle correspondant. Mais ce modèle ne déclenche pas les signaux *_save et *_delete.

Donc si vous voulez réagir à Model.objects.add(), clear() et consorts, il va falloir se mapper sur m2m_changed, un espèce de fourre tout qui gère l’intégralité des cas de figures.

Il s’utilise comme les autres:

def handler(sender, **kwargs):
   # truc

m2m_changed.connect(handler)

Mais déjà, première différence, si vous voulez filtrer sur le sender, il faut utiliser le modèle autogénéré de la troisième table qui est un attribut de l’attribut du modèle qui définit la relation many to many. Vous suivez ? Non ? Relisez la phrase. Ça donne ça:


class Tic(object):

     partenaire = models.ManyToManyField(Tac)

def handler(sender, **kwargs):
   # truc

m2m_changed.connect(handler, sender=Tic.partenaire.through)

through contenant toujours le modèle voulu.

Ensuite, et c’est là la partie bien relou, kwargs va contenir en plus de instance et using:

action

Ce qui arrive pendant le signal. En gros au lieu d’avoir 6 signaux, on en a un auquel on passe la valeur: “pre_add, post_add, pre_remove, post_remove, pre_clear, post_clear“. Du coup votre code doit gérer tous les cas dans une seule fonction à grand coup de if/else. Génial !

reverse

Indique dans quel sens est la relation. Hyper confusionant.

En gros, si j’ai:

class Tic(object):
     partenaire = models.ManyToManyField(Tac)

reverse est sur False, si on part de Tic pour aller vers Tac, et True si on part de Tac pour aller vers Tic, car le ManyToManyField est déclaré dans Tic. C’est complètement arbitraire, car il n’y a bien entendu aucun sens à une relation M2M, c’est justement ce qui la différencie d’un M2One.

Et là où ça devient vraiment fendard, c’est que la valeur des autres paramètres changent selon la valeur de reverse. Il va falloir rajouter des if/else dans vos if/else.

model

La classe qui a été ajoutée, retirée ou wipée de la relation. Dans notre cas, si reverse, c’est Tic, sinon, c’est Tac.

pk_set

Donc le cas des actions add et remove (mais pas clear), ceci est la liste des objets concernés. Elles sont des id d’instances de Tic, si reverse, sinon des id d’instances de Tac.

Créer son propre signal

C’est tellement facile qu’au début on est pas sûr d’avoir tout fait.

Supposons que vous voulez faire une application qui envoie un POUET ! et que vous vouliez également permettre à une autre personne de réagir à cet événement indispensable.

Dans un fichier signals.py, vous allez écrire:

import django.dispatch

pre_pouet = django.dispatch.Signal(providing_args=["pouet"])
post_pouet = django.dispatch.Signal(providing_args=["pouet"])

Ouai, c’est tout. Vous avez créé deux signaux importables qui attendent un argument: le pouet.

Et pour déclencher ces signaux, c’est très simple:

from signals import pre_pouet, post_pouet

class PouetGenerator(object):
    ...

    def send_pouet(self, pouet="Alors là, je dis pouet !"):

        pre_pouet.send(sender=self, pouet=pouet)
        print pouet
        post_pouet.send(sender=self, pouet=pouet)

C’était vachement dur !

Et si quelqu’un veut réagir à vos pouets, il peut faire:

def pouet_handler(sender, **kwargs):
    print "Tiens, un pouet !"

post_pouet.connect(pouet_handler)

Dans la série des subtilités, vous avez, en plus de send(), la possibilités d’appeler send_robust(). C’est la même chose, mais les exceptions sont attrapées ce qui permet à tous handlers de recevoir le signal, même en cas d’erreur. Le sender reçoit les exceptions dans un tuple à la fin.

A ce stade là, vous aurez compris, handler, receiver et callback désignent la même chose: la fonction qui réagit à l’événement. Je les ai utilisées un peu partout sans y prendre garde dans l’article, donc je mets cette note pour le cas où je vous ai perdu.

Last word

Signal.disconnect() est l’inverse de connect(). Je ne m’en suis jamais servis.

Dessin représentant Speedy Gonzales

Ce matin je lançais ma petit recherche Python/Django/Git habituelle sur Twitter pour voir ce qui s’y tramait, quand je suis tombé sur plusieurs tweets qui m’ont fait tiquer.

Il y en a un qui résume très bien l’idée:

Cay de la merde je préfère les langages plus bas niveau, Python c’est lent.

C’est un argument que je lis souvent, et qui est aussi utilisé contre Ruby ou PHP.

Si j’ai la personne en face de moi, généralement la question qui suit est:

Ouai je comprends. C’est quoi ta contrainte ? Tu dois exécuter quel calcul sous quelle limite de temps ?

Il n’y a jamais aucune réponse autre qu’un bafouillement, car une personne qui a une réelle contrainte de temps d’exécution ne sort pas ce genre d’ânerie: il utilise le bon outil pour le bon travail selon des metrics précises, pas selon le nom du langage.

Ceux qui trollent, généralement des étudiants en informatique qui n’ont encore jamais codé d’utile de leur vie (je l’ai fait, donc je +1, d’ailleurs parfois je le fais toujours :-p), ont entendu / lu que Python, Ruby et PHP étaient lents, et donc les rejette car ils ne correspondent pas à l’idée qu’il se fait de la programmation, encore naissante dans sa tête.

La vérité est que les personnes qui ont des contraintes de temps d’éxécution auquel langage X, n’importe quel langage, ne peut pas répondre, font partie des 0.00000001 de la population des programmeurs: le plus souvent dans l’embarqué (voiture, satellite, microchips, etc) et les systèmes temps réels (navigation, chaîne de productions, bourse d’échange…). Pour les autres, nous, quasiment tous, les problèmes de performances se joueront sur l’algo, les libs, les serveurs Web et BDD, le caching, etc. On peut programmer en Basic ou en GOTO++, ça ne change rien.

Languages, libraries and frameworks don’t scale. Architectures do.

Cal Henderson

Dans certains cas, par exemple le calcul scientifique, les interfaces graphiques ou les jeux videos, la rapidité est importante, et on le sait sans même mesurer. Mais ces problèmes-là sont résolus depuis longtemps: il existe des bindings en C extrêmement rapides qui permettent de coder dans son langage interprété favori tout en bénéficiant d’algos Speedy Gonzalez sous le capot.

Par ailleurs, ça a déjà été dit 1000 fois, mais ça ne fait pas de mal de le répéter:

Le salaire du développeur et l’impact commercial de la lenteur d’un développement coûtent immensément plus cher qu’un ordinateur plus puissant. Qui d’ailleurs ne coûtera plus rien dans 6 mois.

Alors OK, ce n’est pas une invitation à coder avec ses pieds sous prétexte qu’on la puissance à disposition (ce qui se fait malheureusement de plus en plus).

Mais choisissez une techno parce que vous êtes productif avec, pas pour ses performances. Sauf si vous avez des mesures chiffrées qui s’imposent à vous. Auquel cas vous n’avez de toute façon pas besoin de lire cet article, vous êtes plus compétent que moi.

Photo-montage d'une jeune fille coursé par un pédobeatr sur un tail

Ce matin dans le train, j’essayais de trouver l’inspiration pour un article de cul. Rien ne vint.

C’est que c’est dur pondre une news par jour, vous vous rendez pas compte. Faire un excellent article une fois, c’est facile. On a tous une étincelle de génie un jour ou l’autre. Mais être bon tous les jours, je cherche encore comment faire.

C’est entre autre pour ça qu’il est vachement plus impressionnant d’être un bon acteur de théâtre que de ciné.

Mais bon, cette fois le Dieu du porno infantile est venu à mon secours, sous la forme ici incarnée de deux petites voisines de 8 ans, qui me permettront de combler le vide d’aujourd’hui en vous reportant leur conversation:

  • tu as vu sa culotte ?, dit la première, tendant le chef-d’œuvre d’art néo-naïf en pastel qu’elle venait juste de terminer. Il représentait une humanoïde femelle à la jupe triangulaire fort drue, rouge et courte. Et avec pas mal de fils qui dépassaient.
  • nan, j’vois pas !
  •  j’l’ai pas dessiné !

Merci les enfants !

Photo du castor qui s'assome avec un arbre

Les versions d’Ubuntu sont de plus en plus instables avec le temps.

Premier laptop (update): pas de touchpad (obligé de le faire passer pour une souris USB avec une option dans un fichier de config GRUB) et pas de wifi (obligé de trouver et compiler le drivers à la main). Je vous passe les détails sur mon après-midi pour avoir trouvé les solutions à ces problèmes.

Deuxième laptop (fresh install): plus de gestion du second moniteur. 4 crash de 4 applications différentes en une demie-heure: 2 sous Unity, et 2 sous Gnome-shell.

Le daemon Ubuntu one est toujours aussi gourmand dès qu’il a plus de 3Go à indexer, et inkillable.

Je joue actuellement avec l’idée de retourner sur une debian et un bureau GNOME 2. J’ai quand même installé KDE pour faire mon ouvert d’esprit. J’ai refermé mon esprit après l’impression de viol graphique que j’ai ressenti à la connection.

Si un dev Ubuntu passe par là: les gars, arrêtez de rajouter des features de Web social et des fenêtres qui font wizz et prenez les 2 prochaines versions pour faire un produit stable. Sans nouvelles fonctionnalités. Justes les dernières libs (pas comme debian), mais un ensemble stable.

Pas la peine de de suggérer les autres distribs/bureaux, ceci n’est pas une appel à une solution à mon problème.

P.S: arrête de rire Max. Je te pisse à la raie.

P.P.S: vous noterez que le passage à Python 3 ne s’est pas fait. Et oui, c’est pas si simple

Photo retouchée d'une question de "Qui veut gagner des millions"

Bonjour M. Gentil. Vous venez pour la position de stagiaire ingénieur senior en periode d’essai sur 3 ans ?

C’est bien, c’est bien.

Nous avons des perspectives de progression fascinantes dans notre SS3I au carré.

J’ai juste quelques tests à vous faire passer. Trois fois rien. Simple formalité administrative. Vous comprenez, on ne peut pas embaucher pas n’importe quel Bac + 5 et le payer SMIC, comme ça sur un coup de tête.

Ce n’est pas contre vous, non.

Pourriez-vous me dire ce qu’affiche ce snippet ? (mouahahahahaha, rire diabolique intérieur)

def test():

    try:
        return 1 + "1"
    except TypeError:
        return "exception"
    finally:
        return "finally"

print test()

Et celui-là, il affiche la stack trace ou pas ?

def test():

    try:
        print 1 + "1"
    except TypeError:
        raise ValueError('Test')
    finally:
        return "finally"

print test()

Bon ok, mais si on a un générateur alors ?

def test():

    try:
        yield 1 + "1"
    except TypeError:
        yield 'typerror'
        return
    finally:
        yield "finally"
        return

    yield "Out"

for value in test():
    print value

Quelle exception sera catchée, dans le bout de code suivant ? (tapoter son style de manière énervante sur la table)

def test():

    try:
        assert 1 + "1"
    except AssertionError:
        print "assertionerror"
    except Exception:
        print "exception"
    except TypeError:
        print "typerror"

test()

Je vois. Donc finally est exécuté dans tous les cas ?

def test():

    def foo():
        foo()

    try:
        foo()
    except RuntimeError:
        print 'runtimeerror'
    finally:
        print 'finally'

test()

Vous êtes sûr ? VRAIMENT dans tous les cas ? (regard appuyé bien stressant, travaillé durant un poste de manager chez Quick)

def test():

    try:
        PRINTEUH !
    except SyntaxError:
        print 'syntaxerror'
    finally:
        print 'finally'

test()

VRAIMENT, VRAIMENT, dans tous les cas ? Mais alors, VRAIMENT ? (prendre la voix d’Alain Chabat, parce qu’arrivé à ce stade là c’est juste plus rigolo)

def test():

    def foo(bar):
        print bar
        return foo, bar

    bar = 'bar'

    try:
        while True:
            foo, bar = foo(bar)

    except RuntimeError:
        print 'runtimeerror'
    finally:
        print 'finally'

test()

Ne vous inquiétez pas, nous ne vous JUgeons paaaaaaas. Détendez-vous. Allez. Une petite dernière. Je vous aide. sys.exit(1) retourne le code 1.

import sys

def test(l=[]):

    print l
    try:
        return 1 + "1"
    except TypeError:
        return l or l.append(sys.exit(1))
    finally:
        print "Je sais plus là, sérieux"
        if len(l) < 1:
            test()

test()
test()

Bon, je sens que vous êtes fatigué. On va arrêter là peut être. Vous êtes le genre à vous en tenir là, hein ? Non. Très bien, très bien. Voici un code sur lequel on planche en interne depuis une semaine pour comprendre combien de fois il affiche finally, mais on s'est dit que vous pourriez le résoudre gratuitement pour nous:

import sys
import time

import multiprocessing
from Queue import Empty

in_queue = multiprocessing.Queue()
out_queue = multiprocessing.Queue()

def worker():

    while True:

        try:
            res = in_queue.get(timeout=0.1)
            if res == 'stop':
                sys.exit(1)
            print res
        except (Empty, multiprocessing.TimeoutError):
            pass
        finally:
            print 'finally'


process = multiprocessing.Process(target=worker)
process.start()


in_queue.put('test')
in_queue.put('stop')

print 'afterstop'

time.sleep(1)

print 'done'

Merci, ce sera tout.

Bon WE, monsieur Gentil.

Nous gardons votre CV. On vous recontactera.

* * * * * serve beer

Si vous utilisez crontab pour vos tâche, faites attention à un détail : le PATH.

il y a un fichier de conf que j’ai pas réussi à faire fonctionner dans /etc/crontab, si vous y arrivez tant mieux sinon éditez votre cron de la sorte:

1. Version avec PATH défini par tâche:

30 23 * * * PATH=$PATH:/usr/local/bin && export PATH && python /home/penetrator/script.py

2. Version avec variables définies en haut du fichier cron:

# Env
SHELL=/bin/bash
HOME=/home/prod
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin

# Cmd cron
30 23 * * * python /home/penetrator/script.py

$PATH=PATH:/usr/local/bin && export PATH va rajouter /usr/local/bin dans le PATH.
Ayant installé et compilé un programme dans /usr/local/bin et l’utilisant dans script.py, il faut que son chemin soit dans le PATH.

Pour avoir des infos sur l’env de votre cron:

* * * * * PATH=$PATH:/usr/local/bin && export PATH && env > /tmp/cronenv

faites un tail -f /tmp/cronenv pour voir si votre PATH est bien déclaré.

ça peut éviter quelques heures à s’arracher les cheveux pour le debug d’un script…

Dessin d'un homme serrant ses couilles dans un étau

Si comme moi vous ne vous souvenez jamais de l’incantation magique qui permet de décompresser les fichiers tar.gz et autres linuxeries, voici un pense-bête:

tar xvf truc.tar # untar uniquement
tar xvzf truc.tar.gz # decompresse sur place
tar xvzf truc.tar.gz /chemin/vers/dossier # decompresse dans un dossier
tar jxf truc.tar.bz2 # decompresser bz2
gunzip truc.bz > truc # ungzip uniquement

Et pour les zip/tar, c’est unzip/untar, sans option. Mais il faut les installer à part.

Git animé montrant le coyote avec un panneau "oups"

Par curiosité nous voulions savoir combien de personnes allaient sur 0bin. C’était une mauvaise idée.

On ne pouvait pas utiliser Google Analytics car ça aurait émoussé la confiance des utilisateurs, alors on a installé notre propre instance de Piwik: une solution open source sur laquelle du coup nous avions la main du code en passant par le serveur.

Malheureusement, en regardant les stats détaillées, j’ai remarqué que Piwik sauvegardait chaque URL avec l’ancre, donc avec la clé de chiffrement. J’avais dans ma base de données une liste d’URLs menant à des pastes dont je pouvais lire le contenu.

Cela pose plusieurs problèmes:

  • si vous êtes hébergeurs de 0bin, vous voulez éviter ça à tout prix. Le but est justement ne pas pouvoir faire ça, car on parie sur le fait que ce qu’on ne peut pas être légalement tenu de modérer ce qu’on ne peut pas lire.
  • si vous êtes utilisateurs, tout l’intérêt de ce qui fait 0bin autre chose qu’un simple pastebin s’écroule

J’ai tout supprimé, mais c’est vraiment un coup de bol que je m’en sois aperçu, car je n’avais même pas imaginé que juste pour des stats on pourrait enregistrer jusqu’à l’ancre d’une URL. Et je n’ai certainement pas cherché ces URLs, qui n’étaient pas mises en avant dans ma configuration du tableau de bord.

Moralité:

  • si vous êtes hébergeurs, n’installez rien sur 0bin. Rien du tout. On ne peut jamais être parfaitement certain de ce que fait un outil tierce partie, fut-il open source. Si je n’avais pas ouvert par hasard ce sous-sous-sous-menu, je n’aurais pas vu le problème, et aurait pu accumuler ces URLs pendant des mois.
  • si vous êtes utilisateurs, faites bien attention à ce que l’outil ne comporte rien en plus. Un petit view source est de rigueur. Si quelqu’un peut nous pondre une extension pour vérifier le JS, encore mieux…