Hier j’ai eu rencontré le travail d’une de ces fameuses personnes qui pensent que la ré-utilisabilité c’est pour les pédés, et qui font des scripts dont la moitié des infos renvoyées sont printées au milieu de blocs de code de 50 lignes, sans possibilité de les récupérer.
Heureusement, avec un petit hack, on peut capturer ce qu’affiche un autre code, et sauver le bébé, l’eau du bain, et même le canard en plastique.
Le code pour les gens pressés
J’ai enrobé l’astuce dans un context manager, ça rend l’utilisation plus simple.
import sys from io import BytesIO from contextlib import contextmanager @contextmanager def capture_ouput(stdout_to=None, stderr_to=None): try: stdout, stderr = sys.stdout, sys.stderr sys.stdout = c1 = stdout_to or BytesIO() sys.stderr = c2 = stderr_to or BytesIO() yield c1, c2 finally: sys.stdout = stdout sys.stderr = stderr try: c1.flush() c1.seek(0) except (ValueError, IOError): pass try: c2.flush() c2.seek(0) except (ValueError, IOError): pass |
Notez l’usage de yield.
Et ça s’utilise comme ça:
with capture_output() as stdout, stderr: fonction_qui_fait_que_printer_la_biatch() print stdout.read() # on récupère le contenu des prints |
Attention, le code n’est pas thread safe, c’est fait pour hacker un code crade, pas pour devenir une institution. Mais c’est fort pratique dans notre cas précis.
Comment ça marche ?
stdin
(entrée standard), stdout
(sortie standard) et stderr
(sortie des erreurs) sont des file like objects, c’est à dire qu’ils implémentent l’interface d’un objet fichier: on peut les ouvrir, les lire, y écrire et les fermer avec des méthodes portant le même nom et acceptant les mêmes paramètres.
L’avantage d’avoir une interface commune, c’est qu’on peut du coup échanger un file like objet par un autre.
Par exemple on peut faire ceci:
import sys log = open('/tmp/log', 'w') sys.stdout = log # hop, on hijack la sortie standard print "Hello" log.close() |
Comme print
écrit dans stdout
, en remplaçant stdout
par un fichier, print
va du coup écrire dans le fichier.
Mais ce code est fort dangereux, car il remplace stdout
de manière définitive. Du coup, si du code print
après, il va écrire dans le fichier, même les libs externes, car stdout
est le même pour tout le monde dans le process Python courant.
Du coup, il est de bon ton de s’assurer la restauration de stdout
à son état d’origine:
import sys log = open('/tmp/log', 'w') bak = sys.stdout # on sauvegarde l'ancien stdout sys.stdout = log print "Hello" log.close() sys.stdout = bak # on restore stdout |
Comme je le disais plus haut, ceci n’est évidement pas thread safe, puisqu’entre la hijacking et la restoration de stdout
, un autre thread peut faire un print
.
Dans notre context manager, on utilise BytesIO()
et non un fichier. BytesIO
est un file like objet qui permet de récupérer un flux de bits en mémoire. Donc on fait écrire print
dedans, ainsi on a tout ce qu’on affiche qui se sauvegarde en mémoire.
Bien entendu, vous pouvez créé vos propres file like objects, par exemple un objet qui affiche à l’écran ET capture la sortie. Par exemple, pour mitiger le problème de l’absence de thread safe: 99% des libs n’ont pas besoin du vrai stdout
, juste d’un truc qui print
.
import sys from io import BytesIO class PersistentStdout(object): old_stdout = sys.stdout def __init__(self): self.memory = BytesIO() def write(self, s): self.memory.write(s) self.old_stdout.write(s) old_stdout = sys.stdout sys.stdout = PersistentStdout() print "test" # ceci est capturé et affiché sys.stdout.memory.seek(0) res = sys.stdout.memory.read() sys.stdout = PersistentStdout.old_stdout print res # résultat de la capture |
Pour cette raison le code du context manager permet de passer le file like objet à utiliser en argument. On notera aussi que si on souhaite rediriger stdout
mais pas stderr
et vice-versa, il suffit de passer sys.stdout
et sys.stderr
en argument :-)
Détourner sys.stdout/stderr est aussi un truc pratique pour tester les sorties d’un code qui fait des
print
à juste titre. Un petit coup demock.patch('sys.stdout', new_callable=BytesIO)
et hop !Pour le tests c’est vrai que c’est pratique.
Pour ceux qui ne savent pas de quoi Soli parle, la lib mock (pip install mock ou http://pypi.python.org/pypi/mock/), et une lib qui permet de faire tout un tas de simulations avec les objets en Python à des fins de tests.
En l’occurence
mock.patch
:http://www.voidspace.org.uk/python/mock/patch.html
sert aussi de décorateur et context manager, et est une généralisation de l’astuce de cet article.
ah les canards en plastique …
Je ne peux pas lire l’article parce que je navigue avec javascript désactivé. C’est quoi cette merde sérieux ?
Le plugin qui met le site en version mobile déconne parfois et se déclenche pour rien. Il requiert javascript.
J’ai aucune idée de comment résoudre ça, wordpress marche ou pas, mais il n’y a pas moyen de réparer sans passer des jours dans un code spagetti merdique immaintenable.
Dans le même style (tu pourrais en faire un article), y a moyen de remplir automatiquement les entrées utilisateur.
Genre une fonction qui requiert une entrée une utilisateur:
import sys
sys.stdin = type('ClassName', (), {'readline': lambda: "ROUGE"})
color = input("Quelle couleur ?")
print(color)
sys.stdin = sys.__stdin__
L’idée est LARGEMENT améliorable :) mais c’est super bon à savoir.
PS: J’ai utilisé une class dynamique pour gagner de la place.
Enjoy.
Bonne idée, mais input() est pas bloquant ? Du coup faudrait un thread non ? J’ai pas testé, donc je dis ça au pif.
Justement. Le problème à la base venait de là … impossible d’effectuer des tests sur une fonction faisant des inputs.
Du coup j’ai investiguer sur comment fonctionnait input en background : il exécute la méthode readline de stdin.
Enfaite c’est cette méthode qui est bloquante :) si on écrase stdin en lui donnant une méthode non bloquante, alors il prendra directement la valeur qu’on lui donne (comme dans mon exemple).