PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Utilisez des variables globales avec JSLint

mercredi 17 avril 2013 à 18:13

Petite astuce si vous utilisez JSlint (par exemple via le super plugin Sublime Text).

Il va vous mettre en avant toutes les variables globales ou non déclarées précédement. C’est génial la plupart du temps, mais c’est un peu chiant pour des variables qui sont volontairement globales et mises à dispo par d’autres scripts comme les frameworks et libs.

Par exemple, il va vous déclarer que jQuery est une variable globale ou non déclarée. Pour éviter ça, mettez ce commentaire tout en haut du fichier :

/*global jQuery:true, $:true */

JSLint va le prendre en compte et ignorer ces variables.

On peut lister autant de variables qu’on le souhaite.

flattr this!

Hic

mercredi 17 avril 2013 à 01:41

Bon, il est 1 h du mat, et je suis bourré.

N’ayant pas pondu l’article quotidien, j’ai tenté une approche artisanale en rentrant du bar pour taper un truc vite fait mais j’ai l’impression d’avoir un clavier en VREFTY.

Bref, je crois qu’il vaut mieux que je tape rien plutôt que de la merde.

Et je me dis qu’il vaut mieux ne rien publier que cet article inutile, mais en même temps ça fait 20 minutes que j’essaye de le taper, alors maintenant qu’il est là, j’ai pas envie de le jetter.

Pardon aux familles, tout ça.

flattr this!

Décorateur de trace

lundi 15 avril 2013 à 19:33

(Ceci est un post invité de xonop sous licence creative common 3.0 unported.)

Suite aux superbes articles sur les décorateurs et sur l’écriture des logs en python j’ai voulu mettre en pratique dans mon projet.
C’est là que les ennuis ont commencé !

Objectif :

Créer un décorateur qui me permette de tracer le passage dans certaines méthodes.
Celui-ci doit :

Première étape : le logger

Avant toute chose mettons en place l’environnement pour pouvoir tracer en utilisant le module logging.
Pour simplifier les exemples, nous associons un seul handler de type terminal.
La fonction log_debug permet de faire appel au logger pour tracer une information.

logger.py :

import functools
import logging
 
__DECO_ACTIVATED = True
__logger = None
 
def init_logger():
    global __logger
    __logger = logging.getLogger()
    __logger.setLevel(logging.DEBUG)
    terminalHandler = logging.StreamHandler()
    terminalHandler.setLevel(logging.DEBUG)
    __logger.addHandler(terminalHandler)
 
def log_debug(text):
    __logger.debug(text)

Deuxième étape : le décorateur, version basique

Pour commencer, le décorateur débrayable :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        __logger.debug("BEGIN")
        data = func(*args, **kwargs)
        __logger.debug("END")
        return data
    return wrapped

Remarques :

Et maintenant un module pour tester ça :

main.py :

import logger
 
class Generic():
    @logger.log_decorator
    def do_it(self, arg1):
        logger.log_debug(arg1)
 
if __name__ == "__main__":
    logger.init_logger()
    generic = Generic()
    generic.do_it("NOW")

Et voilà le résultat :

BEGIN
NOW
END

Troisième étape : les noms de module et de fonction

Ces informations se récupèrent facilement, voici la nouvelle version du décorateur :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__    func_name = func.__name__ 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        msg = "Module={} Function={}".format(module_name, func_name)        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
    return wrapped

Et maintenant le résultat :

BEGIN Module=__main__ Function=do_it
NOW
END Module=__main__ Function=do_it

Jusqu’ici tout va bien.

Quatrième étape : le nom de la classe

Et c’est maintenant que les choses se corsent !
Tout d’abord, l’objet func que nous manipulons est une fonction et non une méthode de classe :

print(func)
<function Generic.do_it at 0x00C70348>

En effet lors de l’exécution du décorateur, la classe en tant qu’objet n’existe pas encore.
Il n’y a pas de lien direct entre la fonction et sa future classe.

Bon nombre de développeurs bien intentionnés nous conseillent d’utiliser self pour déterminer sa classe.

Oui mais :

Pour s’en convaincre, voici le nouveau décorateur :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    func_name = func.__name__
 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        try:            class_name = args[0].__class__.__name__        except IndexError:            class_name = ""        msg = "Module={} Class={} Function={}".format(            module_name, class_name, func_name)        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
    return wrapped

Et maintenant le module de tests :

main.py :

import logger
 
class Generic():
    @logger.log_decorator
    def do_it(self, arg1):
        logger.log_debug(arg1)
 
class Specific(Generic):    @logger.log_decorator    def do_it(self, arg1):        super().do_it(arg1) 
if __name__ == "__main__":
    logger.init_logger()
    specific = Specific()    specific.do_it("NOW")

Et le résultat tant attendu :

BEGIN Module=__main__ Class=Specific Function=do_it
BEGIN Module=__main__ Class=Specific Function=do_it
NOW
END Module=__main__ Class=Specific Function=do_it
END Module=__main__ Class=Specific Function=do_it

Et là c’est le drâme, on a perdu la classe Generic ! Mais analysons plutôt l’exécution :

Comme prévu, la classe de l’objet self ne change pas que l’on soit dans une méthode de la classe ou de l’une de ses super-classes.

Quatrième étape : autre approche

Ne pouvant déterminer directement la classe d’appartenance de la fonction, essayons de la chercher dans le module.
Pour commencer il nous faut l’objet module alors que nous ne connaissons que son nom.
Le module inspect propose justement ce service grâce à getmodule.
Voici le décorateur modifié :

logger.py :

import inspect
 
def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)    class_name = "UNKNOWN"    for key, obj in module_obj.__dict__.items():        try:            members = obj.__dict__            method = members[func.__name__]            if method == func:                class_name = key                break        except (KeyError, AttributeError):            pass    func_name = func.__name__
 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
 
    return wrapped

Pour rechercher la fonction, le décorateur doit :

Nous obtenons alors :

BEGIN Module=__main__ Class=UNKNOWN Function=do_it
BEGIN Module=__main__ Class=UNKNOWN Function=do_it
NOW
END Module=__main__ Class=UNKNOWN Function=do_it
END Module=__main__ Class=UNKNOWN Function=do_it

Pas glop ça marche pas !
Regardons le contenu du module avant la recherche :

print(module_obj.__dict__.keys())
dict_keys(['__builtins__', '__name__', '__file__', '__doc__', '__loader__',
           '__cached__', 'logger', '__package__'])

Curieusement les classes n’apparaissent pas, mais c’est tout à fait normal.
Comme vu précédemment, lors de l’exécution du décorateur la classe est en instance de création.
Il faut donc faire cette recherche lors de l’exécution de la fonction wrappée.

Le décorateur devient donc :

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)
    func_name = func.__name__
 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        class_name = "UNKNOWN"        for key, obj in module_obj.__dict__.items():            try:                members = obj.__dict__                method = members[func.__name__]                if method == func:                    class_name = key                    break            except (KeyError, AttributeError):                pass        msg = "Module={} Class={} Function={}".format(            module_name, class_name, func_name)        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
 
    return wrapped

Et le résultat :

BEGIN Module=__main__ Class=UNKNOWN Function=do_it
BEGIN Module=__main__ Class=UNKNOWN Function=do_it
NOW
END Module=__main__ Class=UNKNOWN Function=do_it
END Module=__main__ Class=UNKNOWN Function=do_it

Pas glop 2 le retour !
Faisons appel au débogueur suprême : print

key    = Generic
method = <function Generic.do_it at 0x00D07C90>
func   = <function Specific.do_it at 0x00D07CD8>
 
key    = Specific
method = <function Specific.do_it at 0x00D07D20>
func   = <function Specific.do_it at 0x00D07CD8>

Effectivement les références ne correspondent pas, et encore une fois, rien de plus normal.
La fonction d’origine a été wrappée donc celle présente dans le module n’est plus celle d’origine.
Qu’à cela ne tienne, recherchons-là !

Après la déclaration de la fonction wrapped, le décorateur la mémorise dans la variable wrapped_function.
Elle sera utilisée à l’exécution de la fonction.

logger.py :

def log_decorator(func):
    if not __DECO_ACTIVATED:
        return func
    module_name = func.__module__
    module_obj = inspect.getmodule(func)
    func_name = func.__name__
 
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        class_name = "UNKNOWN"
        for key, obj in module_obj.__dict__.items():
            try:
                members = obj.__dict__
                method = members[func.__name__]
                if method == wrapped_function:                    class_name = key
                    break
            except (KeyError, AttributeError):
                pass
        msg = "Module={} Class={} Function={}".format(
            module_name, class_name, func_name)
        __logger.debug("BEGIN " + msg)
        data = func(*args, **kwargs)
        __logger.debug("END " + msg)
        return data
 
    wrapped_function = wrapped    return wrapped

Et maintenant le résultat :

BEGIN Module=__main__ Class=Specific Function=do_it
BEGIN Module=__main__ Class=Generic Function=do_it
NOW
END Module=__main__ Class=Generic Function=do_it
END Module=__main__ Class=Specific Function=do_it

Ouf ! Ca marche !

Au menu du prochain épisode : logger la valeur de certains paramètres passés à la fonction décorée.

Xavier O. avec l’aide précieuse de Laurent B.

flattr this!

Appeler une fonction fabric, hors d’un fichier fabfile

dimanche 14 avril 2013 à 19:57

C’est con mais c’est bon à savoir : si vous avez une un tâche fabric, vous pouvez tout à fait l’appeler en dehors de fabfile dans n’importe quel script Python.

Il suffit de faire dans ce script :

from fabric.api import run, execute, env
from fabfile import la_tache
 
execute(la_tache)

Et on peut changer n’importe paramètre en le passant en keyword à execute. Par exemple pour changer l’host :

hosts = ['user@serveurdistant.com', ...]
execute(la_tache, hosts=[host])

flattr this!

Soyez paranos avec les codes de suppression

samedi 13 avril 2013 à 23:25

L’informatique est généralement une discipline rationnelle (sauf bien entendu dans le cas de l’administration d’un système Windows), et pondre un bout de code pour un cas d’utilisation qui n’aura jamais lieu n’est pas vraiment la qualité première qu’on demande à un développeur.

Sauf. Sauf dans un cas.

Dans le cadre d’un code de suppression.

Au delà de RM

Un code de suppression peut se cacher derrière bien plus qu’un remove :

C’est valable dans les scripts shell, mais également dans les scripts Python (ou tout langage qui vous sert à faire des opérations sur des systèmes critiques) : os.remove, os.rename, shutil.move, open('file', 'w').write('...'), etc.

Mais c’est aussi toutes les requêtes SQL en écriture.

Les codes à surveiller le plus sont ceux qui peuvent entrainer une suppression massive :

Mais tous les autres restent importants si tant est que la donnée cible est critique (contenu de l’utilisateur, donnée non récupérable par un autre moyen ou alors de manière lente et coûteuse). Même quand vous avez un backup dont vous êtes certains de la perfection – et franchement je me méfie toujours des backups – une restauration, c’est long, ça implique souvent une interruption de service ou au moins une dégradation, et de toute façon c’est du taff dont vous n’aviez vraiment pas besoin.

Pensez au petit fils de Murphy et François Perrin

Quand on a un petit site ou un logiciel en local de taille moyenne, on ne pense pas à ce genre de choses. Mais quand on travaille avec des grosses DB qui sont tout le temps sollicitées et des systèmes de fichiers qui sont blindés de Tera octets de contenus qui prennent des semaines à uploader sur un serveur (j’ai bien dis des SEMAINES), on ne veut pas que ça disparaisse.

La réplication et le load balancing peuvent vous sauver d’un plantage matériel ou d’un bug logique. Mais une corruption de données en cascade, ça se propage à tout un système. Et un RM massif, c’est exactement ça, surtout à l’heure des surcouches d’abstraction d’outils de haut niveau automatiques.

Alors vous allez me dire que les chances que ça arrive sont minimes. En fait votre code est propre, vous faites des checks normaux, il n’y a pas d’injections possibles, tout est sanitizé, bref, il n’y a pas de raison que ça chie.

Et vous n’auriez pas tort, si il n’y avait pas 2 importantes particularité du monde de l’informatique :

Et comme la gravité des conséquences d’une merde de suppression massive est du genre “Code Rouge” dans “28 semaines plus tard”, vous ne pouvez pas vous permettre de ne pas blinder les suppressions.

Quelques scénarios à la con qui peuvent vous tuer sur un code aussi con que ça :

from fabric import cd, sudo
 
from django.conf import settings
 
 
...
 
for content in contents:
 
    if not content.published:
 
        with cd(settings.MEDIA_ROOT):
            sudo('rm -fr {}'.format(content.path))

A priori, rien de dangereux : vous contrôlez vos settings, vous contrôlez votre base de données, pas de malveillances, vous vérifiez que le contenu à expiré. Que peut-il arriver de mauvais ?

Plein de choses :

Le plus drôle, c’est quand plusieurs trucs improbables se cumulent, là on peut faire des combos qui déchirent bien toute l’infrastructure.

Les tests unitaires et la sanitization en vous protégera pas de la fatigue, inattention, l’éternuement au dessus en pleine session SSH, le chat qui saute sur le clavier… Souvenez-vous de la mouche dans Brazil.

Vous protéger contre vous même

Sur des codes comme ça, il faut donc prendre des mesures supplémentaires. Faire les vérifications qu’on ne fait jamais, de ce qui ne peut pas arriver.

 
import re
 
from fabric import cd, sudo
 
from django.conf import settings
 
 
...
 
for content in contents:
 
    # stylistiquement on ne fait jamais ça en Python, mais ici, on ne prend
    # pas le risque
    if content.expired == False: # None != False
 
        # on évite autant que possible les CD qui peuvent merder grave
        path = os.path.join(settings.MEDIA_ROOT, content.path)
 
        # on check strictement le chemin qu'on va degommer avec une regex qui
        # ne laisse pas la place "/", "/bidule/*" et autre ".."
        # et qui réplique au plus prêt la structure de l'arbo qu'on vise
        if re.match(r'^regex_de_nazi$', path):
            sudo('rm -fr {}'.format(path))

Idéalement il faudrait avoir les droits nécessaires pour éviter le sudo. On peut même exceptionnellement casser le DRY en mettant MEDIA_ROOT en dur dans cette fonction. C’est à double tranchant, mais on ne vit pas dans un monde idéal.

Une regex qu’on a en production ressemble à ça:

re.match(r’^/[\w-]+/[\w-]+/\d/\d/\d/\d/\d/\d+$’, path):

Qui ne laissera passer qu’une suppression d’un truc qui à la forme “/1nom-de_dossier/una_autre-nom/8/1/9/3/5/890709″. Pas une arbo plus courte ou plus longue. Pas d’étoiles ou de guillemets. Pas un dossier qui ne contienne pas exactement cet enchaînent de sous-dossiers numériques.

Et ce, malgré le fait que path est généré depuis un ID extrait d’un auto ID en DB (non accessible dans l’admin). Parce qu’on ne sait jamais. Et on ne peut pas se permettre que ça pète.

Pareil pour le SQL, n’hésitez pas à faire des requêtes en plus avant le DELETE pour voir si les données sont cohérentes. La plupart du temps on s’en branle si une suppression est lente.

Bref, les bonnes pratiques que sont de bien nettoyer ses données, de bien setter ses permissions et de ne pas tout lancer à 3 heures du matin après une session de debugging de 6 heures au Red Bull sont toujours des priorités. Cependant souvenez vous que vous êtes humains, vous allez faire des conneries, les autres aussi.

Vous allez avoir un système imparfait (non, vous n’avez pas mis des checks et des convertions partout, le 100% ça ne marche que dans les labos de recherche, pas quand on a une dead line dans 15 jours).

Alors soyez paranos avec les codes de suppression.

Ce n’est pas une grosse partie du code, ça ne va pas tuer votre productivité ou plomber votre maintenance, mais contrairement aux autres snippets, vous n’aurez probablement pas de seconde chance pour le debug.

C’est l’histoire d’un mec…

Bon ok, de 3 mecs dans un bagnole : un matheux, un physicien et un informaticien.

A la sortie d’un virage, la voiture dérape et s’arrête deux roues dans le vide.

Les 3 compères sortent de la voiture, un peu choqués, et commencent à discuter :

Le mathématicien : “Il faudrait qu’on calcule la probabilité que ça nous arrive sur ce virage, avec cette voiture, dans ces conditions climatiques.”

Le physicien : “Il faudrait qu’on calcule le coefficient de friction et notre énergie cinétique pour mieux comprendre ce qui nous est arrivé.”

L’informaticien : “On reprend le virage avec la caisse pour voir si c’est reproductible ?”

flattr this!

I'm richer than you! infinity loop