source: Sam et Max
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.
(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 :
__DECO_ACTIVATED
.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 :
self
!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 :
Specific.do_it()
vu que l’objet generic
est une instance de cette classe.Generic.do_it()
par le biais de l’instruction super().do_it()
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.
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])
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.
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 :
SELECT * UPDATE
ou DROP table
rm -fr
ou tout code manipulant un fichier matché avec un glob (*)os.removes
, shutil.rmtree
, tout code qui manipule un fichier dans une boucle ou un appel récursifMais 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.
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 :
return
est inséré dans la propriété content.published
et comme Python retourne None
par défaut, tous les contenu sont supprimés.local_settings
et MEDIA_ROOT
n’a pas la même valeur. Vous effacez dans le répertoire de prod ou lieu du répertoire de test.content.path
est éditable dans l’admin privée de Django. En restaurant un formulaire avec une extension Firefox, il se glisse une étoile que vous ne remarquez pas dans le nom du contenu automatiquement utilisé pour créer le chemin de fichier, et vous faites un rm -fr
avec un beau *
dedans.try
est excécuté, mais pas le code du except
, par contre le finally
a bien tout défoncé. Pardon, nettoyé.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.
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.
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 ?”
Remercions la télé pour le nombre de clichés de cul qu’elle a propagé, et notamment toutes ces parties de jambes en l’air torrides dans des endroits bien loin de votre king size bed. Des fois, vouloir se l’a jouer exotique n’a pas l’effet escompté.
C’était en Espagne, on sortait d’un jacuzzi plein d’oranges (pourquoi il avait foutu des oranges dans le bain ?), et on a rejoint le hammam du Spa. Personne en vu, c’était trop tentant.
Sauf qu’après s’être ramolli dans de l’eau chaudes pleines de bulles, la levrette en rafale dans une salle à 10000 degré dans laquelle on respire de la vapeur d’eau c’est dur. Mon cœur à pas du tout aimé, et j’ai du m’arrêter.
Pour la pipe ça doit être pas mal ceci dit.
Autant la douche, si on a de quoi tenir le pommeau tout seul et un débit d’eau généreux, ça le fait (ma salle de bain est une douche italienne avec une vitre transparente qui donne sur un miroir…). Autant se serrer dans ces putains de baignoires françaises avec les bouts du robinets qui dépassent, c’est mort. Impossible de copuler là dedans.
Le mythe. Le truc le plus bidon par excellence. Supposons que l’on ne se soit pas baigné et donc pas recouvert de sel, les cheveux dégueulasses et un goût d’algue dans la bouche. Admettons qu’il n’y ai pas d’enfants de 6 ans et leurs parents psychorigides qui traînent (voir qu’on soit sur une plage naturiste). Subodorons que nous sommes à l’ombre, sans le soleil qui rouge la peau et sans cette crème de protection dégueulasse qui dégoûterait un teckel de lécher un téton plein de Nutella.
Ça fait beaucoup de conditions, mais soit.
Il reste le sable. Cette saloperie de sable. Qui se glisse partout, même quand on a une serviette qui reste de toute façon 23 seconds en place avant de rejoindre les crabes pendant que chaque grain de mica vous irrite les zones érogènes que vous avez tenté de frictionner. Et si vous avez le malteur d’en avoir qui se met sur la capote…
Vous êtes bourré. Elle aussi. Tout le reste de la fête aussi. Alors personne ne va vous voir aller tranquillement dans la chambre à Nico pour un petit quicky. Cette fameuse chambre dans laquelle il y a tous les manteaux entassés de tous les invités.
Vous foutez tout par terre, et faites fît l’odeur de transpiration qui émane des draps que ce petit con d’étudiant en histoire de l’art n’a pas lavé depuis son mémoire sur l’icônification du consumérisme de 1981 à 1983. Vous ignorez aussi les poils torsadés qui traînent ici et là. Nico est bouclé, c’est peut être des cheveux.
Mais la porte ne ferme pas à clé. Et si ça excite peut être mademoiselle cette idée que quelqu’un peut arriver à tout moment, n’empêche que quand le quelqu’un entre dans la salle pour récupérer son briquet dans la poche de sa veste, elle arrête tout. Et ne reprend pas.
Pas assez bourrée, sans doute.
C’est dégueu, ça pu, on peut s’appuyer sur rien. Je dois vous faire un dessin ? Il vaut mieux rien faire que faire ça, sérieux.
Le seul point fort c’est le côté “yes, je viens de rencontrer une nana, et je peux déjà ma l’envoyer tout de suite”. Allez aux putes, ça vous coûtera à peine plus que le prix de l’entrée et des consos.
Certes, c’est une nécessité logistique. Et oui, tout le monde l’a fait, et on le refera, parce que des fois il y a juste pas le choix, pas le temps ou la motiv. Mais il n’y a pas de position confortable sur ces sièges pourris. Et on finit toujours par appuyer par erreur sur le klaxon. Sauf quand on est à l’arrière où il manque toujours la place pour un membre : le cul, les jambes, la têtes. Deux maximum au choix.
Ou alors il faut une limo. Là, ok.
Qu’on se le dise, la paille, ça gratte. Ça gratte pendant, et ça gratte après. On en a dans les cheveux, dans les vêtements, les chaussures… On en retrouve encore des petits bouts le soir, et avec un peu de chance des morceaux dans le lit le lendemain matin. Alors oui c’est confortable. Oui c’est pratique si la miss est pas allergique, mais bon, si on est amateur de blé, autant le faire dans un champ.
J’ai rien contre les murs. J’adore les murs. Y en a de très bien. On peut faire des tas de choses géniales avec une bonne surface verticale solide et rassurante.
Mais combien de mecs ont la force pour porter une nana pendant tout un coït à la force de ses petits bras ?
Généralement ça donne un truc comme ça:
Étape 1 : lui lancer un regard sauvage, la soulever et la plaquer contre le premier plan opposable qu’on trouve.
Étape 2 : se lancer dans une expédition exploratoire de tout le territoire, occuper les places fortes et adapter son rythme à la campagne.
Étape 3 : comprendre que la meuf n’a aucune intention de s’accrocher avec ses bras et que vos deltoïdes vont faire tout le boulot.
Étape finale : au bout de quelques minutes, ralentir, se rendre compte que ça va finir en flamby et trouver une excuse pour changer de position en gardant la face.
Facultatif : Réaliser que c’est un mur en crépi quand elle hurle que tu lui arrache le dos. Oui, c’est vécu, oui.
Qu’on avait pas vu sous la couette. Très surfait. Et très cher aussi.
Existe également en version tablette.
Il regarde. Et il comprend. Et vous savez qu’il regarde et qu’il comprend. Et il sait que vous savez.
Vous pensez à autre chose, et là le chat saute sur le lit pour bien vous rappeler qu’il est là, comme quand vous travaillez sur votre laptop, sauf que là que ne pouvez pas lui donner un coup de latte car c’est pas VOTRE chat.