PROJET AUTOBLOG


Sam et Max

source: Sam et Max

⇐ retour index

Ecrire une commande Django

mercredi 4 décembre 2013 à 08:01

Quand vous avez à faire un script pour un projet Django, il est pratique de l’avoir sous forme de sous-commande de manage.py : c’est portable d’un projet à l’autre et ça permet d’utiliser l’ORM et les templates sans réglage puisqu’on a accès à tous les settings, automatiquement.

Malheureusement le wrapper de Django destiné à cela date un peu, il est plutôt lourd, pas très souple et utilise des pratiques qui ne sont plus au goût du jour depuis quelques années. Rien de rédhibitoire, mais tout de même.

D’abord, il faut placer sa commande dans un fichier portant le nom de la commande, et dans un package management.commands d’une de vos apps.

Par exemple, si vous voulez faire une commande “nettoyer_godmichets” dans l’application “sex_shop”, il faudra la mettre bien profondément dans le tréfonds de votre projet :

racine
│   ├── sex_shop
│   │   ├── management
│   │   │   ├── commands
│   │   │   │   ├── nettoyer_godmichets.py
│   │   │   │   ├── __init__.py
│   │   │   │   └── __init__.pyc
│   │   │   ├── __init__.py

Notez les fichiers __init__.py, indispendables sinon votre commande ne sera pas trouvée. Oh, et ‘sex_shop’ doit être dans INSTALLED_APPS.

Ensuite, votre commande doit être une classe héritant de BaseCommand. Sa méthode handle() sera appelée automatiquement au lancement de la commande.

from django.core.management.base import BaseCommand
from sex_shop.models import Godmichet
 
class Command(BaseCommand):
 
    def handle(self, *args, **options):
        for god in Godmichet.objects.all():
            god.clean()

Et vous pouvez lancer la commande :

./manage.py nettoyer_godmichets

Néanmoins généralement on voudra avoir un peu de décorum.

 
from optparse import make_option
 
from django.core.management.base import BaseCommand
from sex_shop.models import Godmichet, Marque
 
 
class Command(BaseCommand):
 
    # ici on peut mettre un message d'aide
    help = 'Fait briller dard dard les dards'
 
    # optionellement une aide pour les arguments
    args = 'marque1, [marque2], marque3'
 
    # On peut ajouter des options passables à la commande
    option_list = BaseCommand.option_list + (
        make_option('--dry-run',
            action='store_true',
            dest='dry_run',
            default=False,
            help='Affichage uniquement, aucune action réelle'),
        )
 
    def handle(self, *args, **options):
 
        # on retrouve dans args les arguments positionnels de la commande
 
        if not args:
            for god in Godmichet.objects.all():
                # Plutôt que print(), on peut utiliser ce wrapper
                # pour ecrire sur le terminal. Cela permet de bénéficier
                # automatiquement de l'option --verbosity
                self.stdout.write('Processing %s' % god.name)
 
                # La valeur des options est passée via kwargs.
                if not options['dry_run']:
                    god.clean()
        else:
            # l'utilisateur a passé des marques ? On nettoie que les gods
            # de ces marques...
            for marque in args:
                try:
                    gods = Marque.objects.get(name=marque).gods.all()
                    for god in gods:
                        self.stdout.write('Processing %s' % god.name)
 
                        if not options['dry_run']:
                            god.clean()
 
                except Marque.DoesNotExist:
                    # si la marque n'existe pas, on fait une erreur
                    # ceci arrête le script, retourne un code d'erreur 1
                    # et met le texte en rouge
                    raise CommandError('La marque %s n'existe pas' %s marque)

Et voilà, votre commande accepte maintenant optionnellement qu’on lui passe une liste de marques et/ou une option --dry-run:

./manage.py nettoyer_godmichet devilmaycry choucroutebestfriend --dry-run

En plus de ça, la commande accepte automatiquement :

Qui ont le même effet que sur toutes les commandes django officielles.

flattr this!

Conséquences de print devenant une fonction en Python 3

mardi 3 décembre 2013 à 09:31

Devoir utiliser print() au lieu de print m’arrache la gueule, je dois l’avouer. J’ai l’impression que ces deux parenthèses ma prennent 5 heures à taper, là où avant mon petit espace était à portée de pouce.

Mais évidement, la décision de faire de print une fonction est parfaitement rationnelle, et en voici tous les avantages…

print n’est plus une déclaration, mais une expression

Il y a deux types d’instructions en Python : les déclarations (‘statement’ dans la langue de Chuck Norris) et les expressions.

Les déclarations sont des instructions indépendantes : while, var = 'valeur', try, def, etc. On ne peut pas les mettre dans une expression, seulement les imbriquer dans une autre déclaration.

Les expressions, elles, sont imbricables dans n’importe quoi, et elles retournent toujours quelques chose quelque chose, fusse None. Parmis les expressions on retrouve : les lambdas, les listes en intentions, les appels de fonctions, etc.

Avant, print était une déclaration, ce qui était très peu souple. Maintenant, c’est un appel de fonction, et donc une expression.

On peut donc utiliser print dans une autre expression et ainsi :

Dans une lambda :

crier = lambda x, *a, **k: print(x.upper(), *a, **k)

Dans une liste en intention :

[print(x) for x in range(10)]

Plus de syntaxe bizarre

Comment supprimer le saut de ligne quand on print avec l’ancienne syntaxe ?

print "Hello", # <- notez la virgule
print "You"
Hello You

Avec la nouvelle syntaxe :

print("Hello ", end='')
print('You')

Comment rediriger le print vers stderr avec l’ancienne syntaxe ?

>>> print >> sys.stderr, 'arg !'

Avec la nouvelle ?

>>> print('arg !', file=sys.stderr)

Comment faire une liste séparée par des virgules avec l’ancienne syntaxe ?

>>> l = ['Des petits trous', 'des petits trous', 'toujours des petits trous']
>>> print ', '.join(l)
Des petits trous, des petits trous, toujours des petits trous

Avec la nouvelle ?

>>> print(*l, sep=', ')

En gros, la syntaxe est unifiée, plus besoin de penser à tous ces cas particuliers. Et en plus on peut demander de l’aide avec help(print) alors qu’avant ça faisait une syntaxe error.

On peut récupérer une référence sur
print

Et donc la passer en paramètre pour faire de l’injection de dépendance :

def truc(a, print=print):
    # faire un truc avec a
    print(a); # on peut utiliser print normalement
 
import logging
log = loging.getLogger()
truc(machin, print=log) # on print pas, on log !
truc(autre_machin, print=lambda *a, **k: None) # ignore les prints !

Et également appliquer tous les outils de programmation fonctionnelle de Python :

import sys
from functools import partial
error = partial(print, file=sys.stderr)
error('Wrong !') # va directement sur stderr

Activer print() en Python 2.7

Si vous voulez prendre toute de suite de bonnes habitudes, vous pouvez faire, en Python 2.7 :

from __future__ import print_function

flattr this!

Le point sur le blog (et une mise à jour)

lundi 2 décembre 2013 à 18:43

On a fait pas mal de hors sujets ces derniers temps.

Les articles sur le voyage ont encore du sens sur le blog, puisque Max et moi voyageons beaucoup. Et puis le dernier était très bon.

Mais on commence à être loin de Python et du cul. J’ai rien contre, il faut juste savoir doser.

Du coup il est possible qu’on refuse (ou publie un peu plus tard) certaines contributions.

Comme d’habitude, on répond toujours à tous les mails. Parfois avec des mois de retard. Oui, des mois. Mais on répond (ou alors vous êtes dans nos spams ^^).

Je profite de cet article qui est un grand fourre tout pour remercier ÉNORMÉMENT tous les contributeurs qui ont pondu des articles invités et ont corrigé nos fautes :

Mais aussi tous ceux qui prennent le temps d’écrire des commentaires pertinents. Franchement, la qualité des commentaires sur ce blog est juste hallucinante.

J’espère que j’en oublie pas, sinon vous avez le droit de me fouetter :-)

Histoire de terminer sur une note de joie, j’ai mis à jour le dump du blog, que vous pouvez télécharger ici sous forme de zip.

Grâce à des gentils gens sur Twitter, le dump contient maintenant aussi un backup de tous nos tweets.

flattr this!

Pour vos thumbnails, ImageMagick à la rescousse

dimanche 1 décembre 2013 à 09:11

ImageMagick est la solution de manipulation d’images la plus souple et versatile à disposition. Après pas mal de tests, Max en avait conclu qu’appeler la lib avec un bon subprocess était simplement plus rapide et donnait des résultats de meilleure qualité que d’utiliser PIL en Python ou GD en PHP.

Et je peux vous dire qu’il en a fait des tests pour obtenir le thumbnail des nichons parfait. C’est un perfectionniste quand il s’agit des tétons.

En général, on trouve ImageMagick dans les dépôts. Par exemple, sous debian :

sudo apt-get install imagemagick

Mais il n’est pas rare que Max compile la bête histoire d’avoir la même version partout :

wget http://mirror.checkdomain.de/imagemagick/ImageMagick-6.8.7-4.tar.gz ; tar -xvf ImageMagick-6.8.7-4.tar.gz ; cd ImageMagick-6.8.7-4
./configure
make && make install
ln -s /usr/local/bin/convert /usr/bin/convert
ln -s /usr/local/bin/identify /usr/bin/identify

Après tout ça, vous aurez accès à plusieurs commandes, qui permettent des manipulations d’images diverses.

Par exemple, identify permet de récupérer des informations sur l’image telles que le format, la profondeur des couleurs… Nous on l’utilise surtout pour récupérer la taille :

identify -format "%[fx:w],%[fx:h]" "chemin/vers/image"

On peut utiliser un wrapper tel que wand pour utiliser ça depuis Python. Mais il est très facile de se faire un wrapper vite fait à la main :

import envoy
from minibelt import normalize
 
class ImageMagicError(Exception):
    def __init__(self, result, encoding):
        msg = normalize(result.std_err.decode(encoding))
        super(ImageMagicError, self).__init__(msg)
 
def size(img, encoding=sys.stdout.encoding):
    r = envoy.run('identify -format "%[fx:w],%[fx:h]" "{}"'.format(img))
 
    if r.status_code != 0:
        raise ImageMagicError(r, encoding)
 
    w, h = r.std_out.strip().split(',')
    return int(w), int(h)

Notez que j’utilise minibelt et envoy (un wrapper pour subprocess), car je suis une grosse feignasse.

Pour faire un thumbnail, la recette est plus compliquée, car ImageMagick est à l’image, ce que Perl est à la programmation. There’s_more_than_one_way_to_do_it ©.

Au final, celui qui nous a donné le meilleur résultat en qualité est cette suite d’arcanes secrètes transmise d’oreille de druide à oreille de druide :

convert chemin/image/originale -thumbnail largeurxhauteur^ -gravity center -extent largeurxhauteur -quality 80 chemin/vers/resultat.jpg

Ceci va redimensionner l’image en conservant le ratio, et si le résultat n’est pas adaptée aux dimensions finales, va en découper une partie tout en préservant le centre de l’image. La sortie sera un jpg avec un taux de qualité de 80%.

Là encore, un petit wrapper ne fait pas de mal :

def thumb(img, width, heigth, output, crop=False, encoding=sys.stdout.encoding):
    if crop:
        if crop is True:
            crop = 'center'
        crop = "-gravity {} -extent {}x{}".format(crop, width, heigth)
    else:
        crop = ''
 
    cmd = ("convert {img} -thumbnail {width}x{heigth}^ {crop} -quality 80 {output}").format(
          img=img, width=width, heigth=heigth, crop=crop, output=output,
    )
 
    r =  envoy.run(cmd)
 
    if r.status_code != 0:
        raise ImageMagicError(r, encoding)
 
    return r

Bref, ce genre de petit bricolage est loin d’être propre, mais il s’est montré fort robuste et efficace au fil des années. Quand on fait des dizaines de milliers de screenshots de films de cul tous les jours, c’est important :-)

flattr this!

Changer le mot de passe du super utilisateur django en ligne de commande

dimanche 1 décembre 2013 à 01:20

Vous avez oublié ce maudit mot de passe ? Impossible de se connecter à l’admin ?

./manage.py changepassword username

Pas besoin d’être root.

Souvenez-vous aussi que vous pouvez quasiment tout faire depuis un shell en faisant :

./manage.py shell

Ce qui vous permet d’importer les modèles de vos apps et faire toutes les querys que vous voulez.

flattr this!