Blog // Exirel.me

Retrouvez tous les articles liés au tag Python via le flux rss dédié à ce tag.

DRY, de la fonction au décorateur

Par Florian Strzelecki - 23:38 - 12.07.2017

Tags : Python, Programmation, Développement, decorator

Prenons un exemple de code python simpliste :

def more(number, i):
    """Add ``number`` and ``i``"""
    return number + i

print(more(5, 2))

Sans grande surprise, ceci affiche dans le terminal le nombre "7". Maintenant, mettons que j'ai envie d'afficher du texte avant de faire l'addition... mais seulement de temps en temps ?

La version simpliste, c'est d'écrire une autre fonction, comme ceci :

def more_verbose(number, i):
    """Print a sentence and return ``more(number, i)``"""
    print('Calling function `more`')
    return more(number, i)

print(more_verbose(5, 2))

Ce qui affiche :

Calling function `more`
7

Et maintenant, qu'est-ce qui se passerait si je voulais faire la même chose avec une autre fonction ?

Limites de sphinx-autodoc

Par Florian Strzelecki - 17:31 - 09.07.2017

Tags : Python, Documentation, sphinx, autodoc

J'utilise Sphinx pour écrire des documentations depuis quelques années maintenant, que ce soit pour des projets Python ou non, et avec le recul j'en viens à en considérer les limites. Elles sont nombreuses, et de plus en plus frustrantes.

Mais pour le moment, je vais me concentrer sur son extension autodoc, qui inspecte le code et permet d'en extraire les docstring. Son but est simple : générer une documentation exhaustive et à jour (ce que j'appelle "la référence technique"), puisqu'elle concorde avec le code source documenté (pour l'exercice, on supposera toujours vrai que les docstrings soient toujours à jour avec le code source).

Ma première expérience de documentation fut avec Doxygen, à l'IUT (entre 2004 et 2006). Ce fut une bonne expérience, et une ouverture sur la documentation et sur son importance pour la qualité.

Sans expérience en la matière, il m'apparut très vite comme une pratique essentielle du métier, bien que ce ne soit pas l'avis de tout le monde. Autant dire qu'il y a de quoi écrire des livres entiers sur la place de la documentation, que ce soit dans notre culture ou dans nos passages à la pratique.

Plus tard, j'ai été amené à utiliser des outils similaires, pour d'autres langages, et pour un lectorat de plus en plus varié. Depuis, j'ai même eu l'occasion de donner plusieurs ateliers et conférences sur le sujet, qui me passionne peut-être même plus que le code lui même.

Python, Makefile, et tests

Par Florian Strzelecki - 19:15 - 24.10.2016

Tags : Python, Bonne pratique, Unit Testing, Code Coverage, Makefile

Depuis quelques temps, j'essaie plusieurs fichiers Makefile pour mes projets en pythons : pour lancer les tests, les validateurs divers, et générer les différents rapports (tests, couverture, et qualité).

Voici ce à quoi j'arrive pour le moment :

# Project tasks
# ==========

test:
    coverage run --source=<package> setup.py test

report: pylint
    coverage html

pylint:
    pylint <package> > pylint.html || exit 0

isort:
    isort -rc <package> test.py

flake8:
    flake8

quality: isort flake8

all: quality test report

En général, je lance simplement avec make all, pour tout faire d'un coup sans me poser de questions. Cela va donc lancer :

Premier constat : inutile pour moi de lancer des tests sur du code qui n'est pas correctement formaté. Le coût d'un code propre est minime voire quasi inexistant - j'ai l'habitude, ça aide beaucoup - alors autant se fixer des règles strictes et s'y tenir.

Second constat : je n'utilise plus (ou presque plus) de lanceur de tests spécifiques (py.test, nosetests, etc.). Pourquoi ? Parce qu'au final, ces outils ne font qu'ajouter de la déco sur les tests, mais ils ne m'aident pas à écrire de meilleurs tests. Je suis habitué à unittest, et la plupart du temps, je n'ai pas besoin d'une explication particulière pour un test.

D'ailleurs, que l'on utilise simplement assert ou les méthodes du framework unittest, un message manuel est souvent préférable à une interprétation du lanceur de tests :

import unittest

class TestIsPositive(unittest.TestCase):
    def test_behavior(self):
        assert is_positive(0) is True, (
            'is_positive must return True with 0')
        assert is_positive(2) is True, (
            'is_positive must return True with > 0')
        self.assertFalse(
            is_positive(-1),
            'is_positive must return False with < 0')

Bien entendu, c'est un cas où le message est un peu accessoire : il faut imaginer des cas plus complexes, où le résultat d'une fonction est d'un type complexe, et donc la raison d'être demande un peu plus qu'une simple "ceci doit être égal à cela".

Quoi qu'il en soit, make all est maintenant ma routine quotidienne, simple, rapide, et efficace.

Assert

Par Florian Strzelecki - 00:02 - 13.10.2015

Tags : Python, Bonne pratique, Unit Testing, ProTips, assert

Le mot clé assert, tout le monde connaît à peu près :

def test_your_function()
    assert my_function() == 'good result', (
        'We expect `good result` here.')

Il permet, dans un test unitaire, de vérifier que tout se passe comme prévu. Il est à noter qu'un assert qui se passe bien est un assert qui ne "fait" rien. Il ne fait quelque chose (lever une exception) que lorsque quelque chose ne va pas.

Généralement, il est utilisé dans les tests unitaires, avec pour but de vérifier un aspect particulier du code - d'où souvent le côté "unitaire" de ces tests.

AssertionError

Il est tout à fait possible de recréer l'effet d'un assert avec le bon raise :

>>> assert False, 'We expect True here, not False.'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

Ceci revient à utiliser raise :

>>> raise AssertionError('We expect True here, not False.')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: We expect True here, not False.

C'est d'ailleurs grâce à cela que le framework unittest peut proposer des méthodes spécifiques pour écrire des tests unitaires. Il se trouve qu'aujourd'hui, d'autres framework de tests en Python reviennent à assert, et préfèrent faire une introspection du code pour proposer une meilleure lecture du résultat du test.

Cela veut dire aussi qu'il est tout à fait possible d'attraper l'exception levée par un assert :

>>> try:
...     assert False, 'Oh no!'
... except AssertionError:
...     print('Error detected.')
... 
Error detected.

Là encore, c'est ce qui permet aux frameworks de tests d'exécuter plusieurs tests à la suite, peu importe qu'ils passent ou qu'ils échouent. assert n'est donc qu'un peu de sucre syntaxique au-dessus d'une fonctionnement somme toute très classique d'exception.

Pas seulement dans les tests.

Partant de là, il est aussi possible d'utiliser assert dans du code en dehors des tests unitaires. Oui, mais comment bien faire ?

Pas comme ça...

Tout d'abords, ne validez jamais les valeurs d'une entrée utilisateur ou d'un système externe avec assert : ce n'est pas son but. Cela veut dire :

Par exemple, si vous devez vérifier que la valeur fournie est bien supérieur à 0, ne faites pas ceci :

assert user_input > 0

Mais cela :

if user_input <= 0:
    raise ValueError(user_input)

assert ne doit jamais servir le fonctionnel.

Il y a une raison simple à cela : l'option -O de python. Voici ce qui se passe lorsque je lance un petit programme sans cette option :

$ python app.py 
Traceback (most recent call last):
  File "app.py", line 7, in <module>
    validate(0)
  File "app.py", line 4, in validate
    assert user_input > 0
AssertionError
$ echo $?
1

Et maintenant avec :

$ python -O app.py
$ echo $?
0

Le assert a été purement et simplement ignoré - ce qui n'est pas du tout le résultat attendu ! Si vous voulez vraiment que votre programme utilise une AssertionError, il vaut mieux la lever vous même plutôt qu'utiliser un assert.

Alors comment ?

Un assert permet de vérifier une condition et de "laisser passer" si elle est vraie : nous pouvons utiliser son pouvoir de ne rien faire si tout se passe bien à notre avantage.

La plupart du temps, une application dispose de deux ou plus niveau de code :

assert est une forme de communication.

Si dans le code de haut niveau nous ne pouvons pas utiliser assert, le code de bas niveau est un excellent candidat pour ça. Par exemple, imaginons une fonction qui divise un nombre par un paramètre donné, mais seulement différent de 0 :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    # process number

Ainsi, un développeur pour s'assurer qu'aucun de ses collègues ne tente d'appeler cette fonction en dehors de son module avec un paramètre incorrect - les développeurs ne lançant que très rarement leur application avec l'option -O.

Personnellement, je l'utilise beaucoup pour valider des keywords argument obligatoire dans du code interne :

def do_something(self, bar=None, **kwargs):
    assert 'foo' in kwargs, 'We expect foo!'
    assert bar is not None, 'We expect a value for bar''
    super().do_something(bar=bar, **kwargs)

De cette façon, je peux faire en sorte de conserver la même signature que la fonction parent, tout en ajoutant des pré-conditions. Cela reste un cas spécifique, et j'invite chacun à faire des choix judicieux et réfléchis.

Digression : couverture de code

Dans un précédent article, je parlais de la couverture de code : assert implique une différence importante pour ladite couverture.

Si vous prenez ces deux bouts de code qui ont l'air identique :

def divide_by(number):
    assert not number == 0, (
        'You can not `divide_by` 0.')
    return CONSTANT / number

def divide_user_input(user_input):
    if user_input == 0:
        raise AssertionError('You can not `divide_by` 0.')
    return CONSTANT / number

Ils peuvent être testés de la même façon :

>>> assert divide_by(1) == CONSTANT
>>> assert divide_user_input(1) == CONSTANT

Cependant, le test de divide_by donnera une couverture de 100%, alors que l'autre fonction n'aura une couverture de seulement 66%.

Pourquoi ? Parce qu'un if génère une branche du code, qu'il faut couvrir aussi, tandis qu'un assert ne génère pas de branche, et n'a pas à être "vérifier" par un test.

C'est une distinction importante, car elle permet de faire la distinction entre :

C'est à vous de juger si une fonction peut être appelée avec des paramètres externes à l'application, ou si cela reste purement en interne. Dans le doute, évitez les assert, mais sinon, c'est un outil très puissant, à la fois pour la simplicité du code, pour éviter des tests "en trop", et, enfin, pour la communication entre développeurs.

Couverture menteuse

Par Florian Strzelecki - 18:49 - 03.10.2015

Tags : Python, Programmation, Bonne pratique, Unit Testing, Code Coverage

Le domaine des tests unitaires est vaste, les approches foisonnent, et les polémiques sont nombreuses. Il en est une toute particulière que j'affectionne qui concerne la couverture de code.

Bien sûr, je ne jure que par une bonne couverture de code. Écrire des tests et les exécuter sans regarder la couverture de code (ou "code coverage" dans le jargon), ne représente pas un grand intérêt pour moi. Si je n'ai pas l'assurance que je teste l'ensemble du code, je me sens comme en hiver sans un bon pull : à découvert devant le froid et la colère des éléments.

La couverture de code est un mensonge.

Cette expression, je l'entends très souvent. Trop souvent même, que ce soit par des gens qui sont contre ou des gens qui sont pour - oui, en 2015, il y a encore des gens qui sont contre la couverture de code, et même contre les tests. Ce qui est un peu triste, d'ailleurs - mais passons.

Cependant, rare sont les fois où j'entends des explications claires ou des exemples d'une "bonne" mesure. Comment y parvenir ? Comment détecter les trous ? Comment mieux prévenir les problèmes ?

Je ne vais pas couvrir l'ensemble des cas possibles, mais je suis tombé sur un cas d'école cette semaine à mon travail.

Une note ou deux avec Markdown

Par Florian Strzelecki - 22:37 - 15.08.2015

Tags : Django, Python, Markdown

J'aime bien mettre des mots ou des phrases en exergue :

L'exergue donne du poids à mon message.

Sur mon blog, j'utilise du Markdown pour la mise en forme de mes articles : c'est une syntaxe assez simple et pas prise de tête. Je lui préfère généralement ReStructuredText, mais pour quelques articles de blog, Markdown est plus léger et largement suffisant.

À propos de RST

ReStructuredText est une syntaxe relativement légère et puissante, notamment utilisée par l'outil de documentation Sphinx.

Mais l'est-il vraiment ? Oui, jusqu'à présent, je n'ai pas eu à me plaindre. Le seul hic, c'est qu'il est peut-être un peu trop léger, et que je me retrouve très souvent à vouloir faire une "admonition", ou un encart.

Et pour cela j'ai besoin d'utiliser une extension ou deux de Markdown...

Utiliser une extension

Pour utiliser une extension avec la bibliothèque python Markdown c'est plutôt simple :

import markdown

html = markdown.markdown('some text', extensions=[ext1, ext2, ... ])

Il suffit de fournir une liste d'extensions, et "ça marche". Cependant, ce n'est pas vraiment pratique lorsque l'on sait que plusieurs éléments vont devoir être formatés avec les mêmes extensions. Dans ce cas là, il vaut mieux passer par un objet markdown.Markdown bien configuré, et d'utiliser sa méthode convert:

import markdown

md = markdown.Markdown(extensions=[ ... ])

html1 = md.convert('first text')
md.reset()
html2 = md.convert('another text')
md.reset()

Et pour Django ?

Personnellement, je crée une instance de Markdown dans mon models.py, là où je vais l'utiliser dans les méthodes save de mes modèles.

Et c'est à peu près tout. Rien de bien sorcier, mais y penser permet de gagner très légèrement en performance lors de la sauvegarde de mes objets.

L'autre idée, c'est d'utiliser deux champs dans un modèle : l'un va contenir les données au format markdown, et l'autre la conversion en HTML :

import markdown

from django.db import models

md = markdown.Markdown(extensions=[
    'markdown.extensions.admonition',
    'markdown.extensions.abbr',
    'markdown.extensions.footnotes'
])


class Entry(models.Model):
    title = models.CharField(max_length=250)
    text = models.TextField()
    _text = models.TextField(editable=False)

    def save(self):
        self._text = md.reset().convert(self.text)
        super(Entry, self).save()

    def get_text_html(self):
        return self._text

De cette façon, je n'ai plus qu'à utiliser ce bout de template là où j'en ai besoin : {{ entry.get_text_html }} et le tour est joué.

O et 0

Par Florian Strzelecki - 21:53 - 07.08.2015

Tags : Python, Programmation, Ma vie, loldev, Erreur

Je crois que je travaille trop, je viens de perdre 5 bonnes minutes sur ce problème là :

>>> [][0:None]
[]
>>> [][O:None]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'O' is not defined

C'est sans doute l'erreur la plus stupide de ma journée (voire de ma semaine). J'ai confondu la lettre o écrite en majuscule, avec le chiffre 0 (zéro)...

Fatigue ? Inattention ? Moment de faiblesse ?

Certainement. Mais j'aime trop mon métier pour m'arrêter à ça.

Subtile compréhension de listes

Par Florian Strzelecki - 23:59 - 04.11.2014

Tags : Python, Documentation, Programmation, list-comprehension, generator expression

Si vous faites du python, un jour ou l'autre, vous tomberez sur ce genre de structure :

new_list = [int(x) for x in list_of_string_values]

Il s'agit de l'expression très connue appelée list-comprehension. Puissante et pratique, elle permet d'améliorer la lisibilité (dans certains cas), et offre quelques outils intéressants pour réduire la complexité des algorithmes.

Mais connaissez-vous les generator-expression ? Et les set et dict comprehension ?

Bon, tout n'est pas toujours disponible dans toutes les versions de python. Alors je vous préviens, moi, je parle de Python 3.4, version que j'utilise désormais au quotidien. La plupart des exemples et concepts peuvent être transposés, d'une façon ou d'une autre, en Python 2.7.

Python logging et log level

Par Florian Strzelecki - 23:59 - 03.11.2014

Tags : Python, Documentation, Programmation, logging

Dans le précédent article, je parlais de l'héritage des loggers, technique très pratique pour mutualiser des comportements ou au contraire de les séparer. Vous avez pu voir comment faire en sorte que deux loggers n'utilisent pas le même niveau de log ou pas les mêmes handlers.

Un point que je n'ai pas abordé (parmi tant d'autres) est la gestion du niveau de log : il existe deux configurations de niveau, puis à l'usage il suffit de choisir la bonne méthode pour le bon niveau de log désiré. Certes, j'ai utilisé le niveau de log des loggers, mais je n'ai pas vraiment utilisé la notion de log level du handler.

Commençons donc par un exemple simple, cette fois-ci avec un seul logger ayant deux handlers :

from logging import getLogger
from logging.config import dictConfig

if __name__ == '__main__':

    dictConfig({
        'version': 1,
        'handlers': {
            'console': {
                'level': 'INFO',
                'class': 'logging.StreamHandler',
            },
            'file': {
                'level': 'INFO',
                'class': 'logging.FileHandler',
                'filename': 'log.txt'
            }
        },
        'loggers': {
            'main': {
                'level': 'INFO',
                'handlers': ['console', 'file'],
            },
        }
    })

    main = getLogger('main')

    main.info('My message.')
    main.error('My error message.')

Ce qui donne comme résultat :

$ python log.py
My message.
My error message.

Et dans le fichier log.txt vous retrouverez exactement les même messages.

Python logging et héritage de loggers

Par Florian Strzelecki - 22:20 - 02.11.2014

Tags : Python, Documentation, Programmation, logging

Prenons un exemple simple, un fichier log.py qui va définir deux loggers : main et main.part. Ces loggers pourraient très bien être définis dans un package main et son sous-module part. Pour l'exemple, nous allons faire beaucoup plus simple :

from logging import getLogger
from logging.config import dictConfig

if __name__ == '__main__':

    dictConfig({
        'version': 1,
        'handlers': {
            'console': {
                'level': 'INFO',
                'class': 'logging.StreamHandler'
            }
        },
        'loggers': {
            'main': {
                'level': 'INFO',
                'handlers': ['console', ],
            }
        }
    })

    main = getLogger('main')
    part = getLogger('main.part')

    main.info('My message on the main logger.')
    part.info('And my second message on the main.part logger.')

Ici, il y a donc :

Si vous faites python log.py c'est ceci qui devrait s'afficher dans votre console :

$ python log.py
My message on the main logger.
And my second message on the main.part logger.

Sauf que si vous retirez la partie dictConfig de ce bout de code, plus rien ne s'affiche. Il y a donc bien quelque chose à regarder de ce côté là.

Configurer pip en fonction du virtualenv

Par Florian Strzelecki - 23:52 - 17.09.2014

Tags : Python, Bonne pratique, virtualenv, ProTips

Si vous développez avec python, alors vous avez sans doute entendu parler de deux outils très pratiques : virtualenv et pip. Le premier permet des environnements python isolés les uns des autres, et le second permet d'installer des paquets python (que ce soit au niveau global ou dans un environnement isolé).

À ce stade de la lecture, si vous ne savez pas ou ne connaissez pas l'un ou l'autre de ces outils, je ne peux que vous conseiller vivement de vous y intéresser.

Personnellement, depuis que j'ai investis un peu de temps à comprendre et à utiliser ces deux outils, je ne peux plus m'en passer. Cela dit, virtualenv, bien que très pratique, est un peu "brut", et je ne l'utilise plus qu'avec un autre outil, virtualenvwrapper, qui est d'ailleurs nécessaire pour la suite de l'article.

Fichier de configuration pour pip

Pour une raison ou une autre, vous pourriez avoir envie de configurer pip, que ce soit une option particulière (suivre les dépendances par exemple, ou toujours mettre à jour), ou d'ajouter un serveur de distribution. Car oui, même si, par défaut, pip utilise le PYthon Package Index, il peut vous arriver de vouloir utiliser un miroir, voire en cas d'utilisation avancée, votre propre serveur/miroir (ce qui peut arriver assez vite quand vous travaillez dans une équipe qui travaille principalement avec des projets en python).

Pour se faire, vous avez deux options :

Comme ajouter la même option à votre ligne de commande est fastidieux (et qu'une erreur est si vite arrivée), vous aurez plus vite fait d'utiliser un fichier de configuration. Pour cela, vous devez créer le créer au bon endroit : par défaut, il s'agit du fichier $HOME/.pip/pip.conf, et pour notre cas il ressemble à ceci :

[global]
index-url = https://pypi.team.my-companie.biz/dev/+simple/

Oui, .biz, je fais ce que je veux, c'est un exemple et il est tard.

Si vous voulez en savoir un peu plus sur ce fichier de configuration, je vous invite à lire la documentation "PIP User Guide: configuration".

Le soucis maintenant, c'est qu'à chaque fois que vous utiliserez pip, vous passerez forcément par ce serveur, et ce n'est pas forcément ce dont vous avez envie. Personnellement, j'ai plusieurs projets sur mon poste de travail, et tous n'utilisent pas la même configuration.

Configurer pip par virtualenv avec virtualenvwrapper

Si vous n'utilisez pas virtualenv wrapper, je ne peux que vous invitez à considérer ou à reconsidérer son usage : c'est un outil pratique, qui permet une grande flexibilité, et vous évite de nombreuses tâches répétitives au quotidien.

Lorsque vous créez un virtualenv (que ce soit directement avec mkvirtualenv ou via un projet avec mkproject), l'environnement installé dispose d'un ensemble de scripts servant de hook à l'activation et à la désactivation de l'environnement. C'est généralement le bon endroit pour ajouter votre touche personnelle.

Celui qui nous intéresse ici est le script postactivate : en le modifiant, vous pourrez exécuter des commandes et configurer des variables d'environnements.

Il se trouve que l'emplacement du fichier de configuration de pip peut être défini par une variable d'environnement : PIP_CONFIG_FILE. Maintenant, si je vous dis qu'à l'activation de votre environnement, une variable VIRTUAL_ENV indique le répertoire où se trouve les fichiers de l'environnement, vous devriez deviner tout seul ce qu'il vous reste à faire... mais voici un exemple concret de fichier postactivate :

#!/bin/bash
# This hook is sourced after this virtualenv is activated.

export PIP_CONFIG_FILE="$VIRTUAL_ENV/.pip/pip.conf"

Une fois le virtualenv activé, vous pouvez connaître l'emplacement du fichier postactivate avec echo $VIRTUAL_ENV/bin/postactivate.

Et tadam, le tour est joué : vous avez maintenant un fichier de configuration pour pip spécifique à votre environnement. Il ne vous reste plus qu'à y indiquer les options spécifiques, et à ne plus vous inquiéter de savoir quelles sont les options à modifier avant de changer de projet.

Revenir à la configuration globale

Lorsque vous désactivez l'environnement, sans action particulière de votre part, vous conservez la variable PIP_CONFIG_FILE. Ce n'est pas un problème en soit si vous ne travaillez que dans des virtualenvs, mais vous avez sans doute envie de faire les choses proprement.

Pour se faire, c'est le script postdeactivate, placé au même endroit que le script postactivate, qui nous intéresse. Il suffit d'y indiquer la valeur par défaut de PIP_CONFIG_FILE:

#!/bin/bash
# This hook is sourced after this virtualenv is deactivated.

export PIP_CONFIG_FILE="$HOME/.pip/pip.conf"

Et de la même façon, vous trouverez ce script en faisant echo $VIRTUAL_ENV/bin/postdeactivate après avoir activé votre virtualenv.

Il ne vous reste plus qu'à travailler sereinement. Même s'il reste encore un tas d'outils pratiques, là dehors. Et que je n'en connais pas autant que ce que j'aimerais. Et de là à les maîtriser... pfiou...

Le jeu de la vie

Par Florian Strzelecki - 12:22 - 05.01.2014

Tags : Python, Programmation, Bonne pratique, Technique, OOP, FP

J'aime la programmation orientée objets (OOP), tout autant que la programmation fonctionnelle (FP), chacune pour des raisons différentes. Je suis toujours un peu triste lorsqu'un développeur critique l'un en disant que l'autre est mieux parce que [inséré ici un argument basé sur une différence de fonctionnement]. L'un n'est pas mieux que l'autre, car les deux impliquent des orientations et des compromis différents.

Dans son article "Start Writing More Classes", par Armin Ronacher, j'aime beaucoup cette note de fin d'article :

Something else I want to mention: what's written above will most likely result in some sort of warmed up discussion in regards to object oriented programming versus something else. Or inheritance versus strategies. Or virtual methods versus method passing. Or whatever else hackernews finds worthy of a discussion this time around.

All of that is entirely irrelevant to the point I'm making which is that monolithic pieces of code are a bad idea.

Pourquoi ? Parce que la plupart du temps, les comparaisons que je peux lire se base sur les erreurs et toutes ces choses parfaitement stupides que les développeurs peuvent faire - et si vous faites ou avez fait de la maintenance sur de vieilles applications, vous avez déjà forcément rencontré ce genres d'abominations. Pour Armin, ce sont les fonctions monolithiques, pour d'autres ce sera autre chose.

TL; DR: Trouver une solution n'est déjà pas facile, faire en sorte qu'elle soit à la fois simple et élégante l'est donc encore moins. Le bon sens n'étant pas la norme, aucun paradigme ne vous mettra à l’abri des singes de l'Enfer qui codent avec les pieds et font absolument n'importe quoi avec des concepts qu'ils ne maîtrisent pas totalement.

Par contre, il est bel et bien nécessaire d'avoir du recul sur les outils que nous utilisons, et de comprendre pourquoi, quand, et comment nous devrions les utiliser.

La logique métier et les managers de Django

Par Florian Strzelecki - 13:26 - 03.10.2013

Tags : Django, Python, Programmation, Bonne pratique, ORM

Lors de la Djangocong 2013 à Belfort, @Evlf a proposé un très bon sujet de réflexions avec sa conférence : Une logique métier bien placée. Son constat est simple : tout bon débutant qui se respecte va avoir tendance à utiliser les fonctions spécifiques de l'ORM dans ses vues Django.

Ce qui est plutôt normal pour un débutant devient problématique aux yeux de quelqu'un d'un peu plus expérimenté : quel est la part de "logique métier" qui se retrouve ainsi dans un espace dédié à la vue, la présentation.

Je rejoins @Evlf sur ses questions : comment bien placer la logique métier ? Pour ma part, je pense qu'il est possible d'éviter bien des ennuis en utilisant correctement les managers de Django, et ces derniers sont donc le sujet de cet article.

UnicodeEncodeError : fichiers, formulaires, django et gunicorn

Par Florian Strzelecki - 17:17 - 09.03.2012

Tags : Django, Python, Programmation, Développement, Charset, Problème, Technique

L'erreur bête et très frustrante du jour vient d'un petit formulaire qui permet de téléverser un fichier quelconque sur le serveur. Vous me direz, c'est la base du formulaire web avec des fichiers, et il ne devrait pas y avoir de soucis particuliers... sauf lorsque lesdits fichiers ont des accents dans le nom de fichier, et qu'une erreur aussi frustrante que difficile à analyser débarque avec ses gros sabots.

Alors, premier réflexe : chercher sur le net, entre Stack Overflow et le tracker de bug de django. Vous pourrez tomber sur ce genre de discussions : UnicodeEncodeError: 'ascii' codec can't encode character.

Cela donne déjà un premier aperçu d'où vient le problème, à savoir, une configuration de locale sur l'environnement serveur. Pour vous en dire plus, voici les symptômes auxquels j'ai été moi-même confrontés :

La première "solution" qui est proposée, c'est que l'environnement serveur soit correctement configuré, avec, entre autre, une modification du fichier /etc/apache2/envvars. Personnellement, j'ai essayé, mais cela ne fonctionne pas.

Bref, que faire dans ces cas là ?

Comprendre et analyser le problème

Je ne suis pas un expert, mais voici ma démarche pour comprendre puis résoudre ce problème.

Tout d'abords, j'ai regardé la locale de la machine à l'aide de la commande locale, et j'ai obtenu ceci :

LANG=fr_FR.UTF-8
LANGUAGE=
LC_CTYPE="fr_FR.UTF-8"
LC_NUMERIC="fr_FR.UTF-8"
LC_TIME="fr_FR.UTF-8"
LC_COLLATE="fr_FR.UTF-8"
LC_MONETARY="fr_FR.UTF-8"
LC_MESSAGES="fr_FR.UTF-8"
LC_PAPER="fr_FR.UTF-8"
LC_NAME="fr_FR.UTF-8"
LC_ADDRESS="fr_FR.UTF-8"
LC_TELEPHONE="fr_FR.UTF-8"
LC_MEASUREMENT="fr_FR.UTF-8"
LC_IDENTIFICATION="fr_FR.UTF-8"
LC_ALL=

Ensuite, j'ai lancé l'interpréteur python, pour en savoir plus :

>>> import locale
>>> locale.getlocale()
(None, None)
>>> locale.getdefaultlocale()
('fr_FR', 'UTF-8')

Bon, je remercie ici mYk et linova sur IRC de m'avoir fait chercher dans ces directions, et de m'avoir proposé ensuite de vérifier la valeur de ces informations là dans le contexte de mon application django. J'ai donc affiché ces valeurs dans un template et voici les valeurs que j'ai obtenues avec le serveur de dev de django :

loc: (None, None)
loc_def: ('fr_FR', 'UTF-8')

Puis la même chose, mais avec gunicorn :

loc: (None, None)
loc_def: (None, None)

À partir de là, j'ai pu enfin comprendre le problème, et comment le résoudre. Merci à linova pour l'idée sur IRC.

Merci encore une fois au chan #djangocong sur freenode !

Résoudre le problème

J'utilise runnit pour gérer mes différentes applications servies par gunicorn, et j'ai donc des petits scripts (comme pour init.d), dans lesquels j'ai ajouté ceci :

export LC_ALL=fr_FR.UTF8

Et c'est tout. Oui, tout à fait, c'était juste ça : la locale n'était pas définie dans ce contexte très particulier, et pas dans un autre, ce qui explique tout mon problème avec ces foutus fichiers. J'ai redémarré les différentes applications, et je n'ai plus constaté de problèmes.

Alors, si vous aussi vous avez ce problème, je vous conseille de suivre la même démarche que moi : vérifiez vos locales, vérifiez vos locales via l'interpréteur python d'une part, puis dans le contexte de votre application plus spécifiquement.

Enfin, allez voir du côté du script de votre serveur, et voyez s'il n'y a pas quelque chose à faire avec.

Python-epub : un projet sous licence libre pour le format epub

Par Florian Strzelecki - 18:05 - 08.03.2012

Tags : Python, Programmation, Format ouvert, Développement, Livre numérique, epub, Technique

Ce n'est pas sans une certain appréhension que je publie ce billet, où je souhaite vous parler de mon travail actuel : la bibliothèque python-epub, placée sous licence libre (LGPL), et disponible officiellement sur pypi.

De l'appréhension, car je n'ai pas la prétention d'être un maître dans l'art du code python d'une part, pas plus qu'être le plus grand expert du format epub d'autre part. Et puis, surtout, c'est la première fois que je publie vraiment du code sous licence libre !

Mais, passons mes états d'âme pour aller au cœur du sujet.

Téléchargement et installation

Tout d'abords, si vous cherchez directement comment l'installer, et avoir une documentation technique, vous pouvez aller sur le site officiel du projet : http://epub.exirel.me. Il est à jour et documente la dernière version publiée (et pas la version en cours de développement).

Pour faire simple : utilisez pip install epub. Si vous voulez la version de développement (je ne garantis pas la stabilité dudit code), vous pouvez faire un clone des sources disponibles en ligne : https://bitbucket.org/exirel/epub.

À quoi ça sert ?

En premier lieu, à ouvrir des fichiers au format epub, dans la version 2 de la spécification (la 3 restant encore trop jeune et trop peu utilisée pour le moment). Qu'est-ce que le format epub ? Il s'agit d'un format ouvert de livre numérique. Il est utilisé de manière majoritaire dans l'édition numérique, et peut être lu par des applications sur mobiles et tablettes, et surtout, sur des liseuses à écran à encre numérique (de type Kobo ou Cybook Odyssey). Je n'entre pas ici dans la polémique autour des autres formats, ce n'est pas le sujet (mais vous pouvez toujours me poser des questions par email, ou via twitter et g+).

Le format Epub 2

Le principe d'un fichier epub, c'est d'avoir un fichier compressé au format Zip, qui contient plusieurs fichiers importants :

Les autres fichiers composent le "vrai" contenu du fichier epub et sont référencés de plusieurs façons, tant par le fichier OPF qui en fait une liste exhaustive, que par le fichier de navigation NCX qui propose une navigation pour l'utilisateur (comme une table des matières, des illustrations, etc.).

Le code python

De son côté, la bibliothèque python-epub permet de représenter les données d'un fichier epub : fichier OPF, fichier NCX, et le contenu du fichier. Elle est même découpée en trois parties :

Ce découpage logique permet une grande souplesse dans l'utilisation de cette bibliothèque. De prime abord, elle ne permet (pas encore) de générer un fichier epub, mais certains éléments sont déjà manipulables en dehors d'un fichier epub pur : c'est le cas pour le format OPF et le format NCX, dont les modules dédiés permettent d'une part de lire ces formats, mais aussi de générer des fichiers dans ces formats.

Le projet

Licence libre : LGPL

Le projet est placé sous la licence libre LGPL : elle correspond à ma volonté, en tant qu'auteur, de rendre libre le fruit de mon travail pour les utilisateurs. Si d'autres développeurs souhaitent utiliser mon travail (avec ou sans modification) ils n'auront pour seule obligation que de donner les mêmes droits sur cette partie là de leur travail (par exemple, dans le cas d'une application, seule ma bibliothèque devra être sous une licence libre compatible, pas l'ensemble de l'application).

Je pense d'ailleurs que c'est la seule chose pour laquelle je n'envisage pas de changement, contrairement au code en lui-même.

Les évolutions à apporter et celles envisagées

Pour le reste – le code concret, mais aussi la documentation – j'envisage beaucoup d'évolutions, et certaines dont je ne suis pas encore sûr de la façon de les implémenter. Tout d'abords, pouvoir créer et modifier des fichiers epub : il est plutôt simple de lire un fichier epub, mais l'écriture pose beaucoup d'autres questions.

En vrac, j'ai listé les améliorations/évolutions suivantes :

Il y a encore beaucoup à faire pour que je sois pleinement fier de ce projet, mais j'avance correctement, je fais attention à la qualité du code, aux tests unitaires, et à la documentation. Je pense donc que ça va dans le bon sens.

Appel à contribution

Pour le moment, je suis le seul développeur sur ce projet, que j'ai initié en début d'année, mais si j'ai choisi une licence libre, c'est bien pour laisser la place à d'autres personnes.

Ce que je cherche ? Les bonnes volontés : contributions au code, des retours d'expérience, des demandes d'ajouts de fonctionnalités, des rapports de bugs, pourquoi pas des traductions de la documentation ?

Comme je le disais en début de billet : je ne suis pas un maître pythoniste, et au vu de mon expérience, je ne doute pas un instant qu'il me reste beaucoup à apprendre. Je prends donc toutes les remarques et critiques constructives, les avis, les optimisations, les conseils... tout !