Votre avis sur ce projet de lib de gestion de configuration Python ?


Question: cette idée m’est venue durant un long trajet. Est-ce que ça vaut le coup d’être codé ? Qu’est-ce que vous en pensez ? Est-ce qu’il y a une grosse faille ? Est-ce que ça n’existe pas déjà ?

Sauvegarder la configuration d’un programme Python tient du challenge. Pas parce que que c’est difficile ou qu’on manque d’outils, mais à cause du trop grand nombre de choix:

  • Pur Python (Django) ?
  • INI (défaut dans la lib standard) ?
  • JSON (sublime-text) ?
  • gconf (Gnome) ?
  • base de registre (Windows) ?

Toutes ces solutions ont des parsers différents, des outputs différents, et il faut se rajouter les checks, le casting et la gestion des erreurs à la main.

Je me prends à rêver à un moyen de lire et écrire la configuration d’un programme en Python, indépendamment de la source:

  • fichier de config JSON
  • fichier de config ini
  • fichier de config YML
  • fichier de config Python
  • fichier de config XML
  • fichier de config CSV
  • gconf
  • base de registre
  • base de données SQL
  • base de données NoSQL (redis, mongo, etc)

Principe

On donne une définition de la configuration qu’on souhaite stocker, comme un modèle d’ORM:

class MaConfiguration(Configuration):
 
    date = DateField(help_text="This is the date")
    nom = TextField(required=True)
    age = IntegerField(default=12) # default peut être un callable
    temperature = FloatField()
    tags = ListField(values_type=str) # values_type est optionnel
    attrs = DictField(values_type=int)
 
    def set_tags():
        # appel quand tag est setté
        # raise validation error
        # ou fait une conversion
 
    def get_tags():
        # appel quand tags est getté
 
    def load_tags():
        # appel quand tags est récupé depuis la source de config
 
    def save-tags():
        # appel quand tags est sauvegardé dans la source de config
 
    class nesting(Configuration):
        # on peut nester les fields à l'infinie
 
        force = IntegerField(checks=RangeCheck(gt=3))
        description = TextField(checks=RegexCheck(r'yeah', error_message=u"I said yeah"))
        pi = FloatField()

Et on l’utilise comme ça:

conf = MaConfiguration('file://path/to/config/file') # ou redis://db:port, etc
print conf.date
print conf.nesting.force
print conf.get('bibi', 'doh') # fallback possible
try:
    print conf.void
except ValueDoesNotExist:
    pass
conf.nom = "boo"
conf.save()

Les données sont évidement castées automatiquement dans le bon type, à la sauvegarde, le type de valeur et la cohérence de la configuration est vérifiée.

Bien sûr on peut sauvegarder dans une autre source/format.

Ce n’est pas un parseur de fichiers, il faut avoir une définition de la structure de la source de configuration pour qu’on puisse la lire. Mais on peut générer une définition (approximative) à partir d’une source de config existante pour se faciliter la tâche.

Hierarchie

Les frameworks tels que symfony utilisent plusieurs niveaux de configuration, le niveau le plus bas écrasant toujours la valeur du niveau le plus haut.

conf = MaConfiguration({'name': 'root',
                        'path': '/path/to/config/file',
                        'children': (
                            {'name': 'child1',
                             'path': '/path/to/subconfig/file',
                             children: (... etc ...)
                            },
                            {'name': 'child2',
                             'path': /path/to/subconfig/file
                            }
                        })
 
with conf.from('/root/child1') as subconf:
    print subconf.bibi

Ca va d’abord chercher dans le sous fichier de config, et si ça n’existe pas, la valeur du parent, en remontant la chaîne jusqu’en haut, si rien n’existe, retourne la valeur par défaut.

Signals

Même principe qu’en Django, mais appliquer à l’objet de configuration:

  • on_load_source
  • on_load_value
  • on_save_value
  • on_change_value
  • on_get_value
  • on_save_source
  • on_save

On peut enregistrer un callback dans le process Python courant pour qu’il soit appelé quand un de ces événements se déclenche.

On peut aussi lancer un daemon qui check pour tout changement dans la source de config et qui appelle un callback (par exemple un web callback ou un message AMQP pour synchro des config de serveurs) au changement de la config.

Templating

Quand on fait des fichiers de settings à redistribuer (ex: fichier de settings par défaut de Django), on veut qu’il soit présenté d’une certaine façon: ordre des variables, texte qui n’a rien à voir avec la config et qui doit être inamovible, etc.

conf.save() doit donc optionnellement accepter un template qui ressemble à un truc comme ça:

%(variable_name)s
%(variable_name)s
%(variable_name)s

# texte fixe

%(variable_name)s

%(section_name)s // pour une intégrer une section complète

%(section_name)s // pour organiser le niveau de nesting
%(variable_name)s
%(variable_name)s

Dans lequel %(variable_name)s sera replacé par l’intégralité de ce qui concerne la variable (nom, valeur, commentaires). On devrait pouvoir aussi demander a dumper une section entière.

Exemples et documentations

conf.save() par défaut ajoute les help_text en tant que commentaires si possibles, afin que le fichier soit documenté.

print conf.exemple() devrait dumper un faux fichier de configuration avec les valeurs par défaut, ou en l’absence de tel, un exemple arbitraire extrapolé du type. Ainsi il est facile de donner une fichier d’exemple dans sa doc.

Extensibilité

Evidément on peut faire ses propres sous classes de Configuration et Field, afin de distribuer des outils réutilisables.

Il faut aussi permettre la gestion de backends pour les parseurs et les dialectes. Un parseur est quelque chose qui récupère les données depuis la source de configuration, et qui les transforme en un arbre de données en objects Python. Un dialecte est ce qui va prendre cet arbre et caster ses valeurs pour lui donner du sens.

Par exemple, le parseur yml lit le fichier et le met sous forme d’arbre, tandis que le dialecte yml parse les types yml (int, string, date) et caste les valeurs.

Ils sont séparés car on peut très bien avoir un parseur et un dialecte différent (ex: parseur XML, dialecte issue d’un DTD particulière)

Cela permettra ainsi à la communauté de contribuer des parseurs et dialectes pour des fichiers de configs particuliers comme ceux d’Apache ou d’Nginx.

On pourra imaginer avoir une banque de parseurs et dialectes, et pouvoir faire des trucs comme ça:

conf = NginxCongiguration('/path/to/nginx/file')

Gestion des erreurs

Un grand soin doit être apporté à la gestion des erreurs, pour rapporter rapidement et clairement à l’utilisateur ce qui n’est pas configuré correcrtement: type, valeur obligatoire, problème de droits, etc. Car la plus grande source d’erreur dans la config, c’est la spécificité (syntax, format, etc) de la source de config elle-même.

Les formats des sources de données ne sont pas compatibles entre eux, il faut donc créer une liste de capabilité pour chacun d’eux et crasher explicitement quand on essaye de faire quelque chose d’impossible (comme parser un yml avec des valeurs nested et sauver le résultat dans un CSV).

Loader ?

Trouver la source de configuration est un travail en soi. Entre le chemin absolu par défaut, le chemin relatif par défaut, ou l’url + port ou la socket par défaut, ou le chemin d’import d’un module Python, c’est tout un truc.

Et que faire si le fichier n’existe pas ? Le créer ? Lever une erreur ? Et si il n’y a pas les droits ?

Que faire si la ressource n’est pas accessible temporairement (un BDD, redis, un lecteur réseaux): attendre jusqu’au timeout, retry, erreur ?

Et que faire si on passe un dossier: scanner récursivement ? Ignore-t-on les fichiers cachés ? Que faire avec les fichiers de mêmes noms ?

Bref, charger le fichier de configuration c’est un gros algo à lui tout seul, et je me demande si c’est le boulot de la lib. Si oui, il faudra créer des loaders et les mettre par défaut selon le type de source. Mais il faut réussir à faire ça sans complexifier l’API. L’outil doit être puissant, mais rester simple.

Rester simple

Justement…

Evidément tout ça peut rendre l’utilisation très lourde, et je suis assez fan des libs comme peewee: simple et efficace, Pareto friendly.

L’idée est qu’une personne doit pouvoir faire ça:

from configlib import Configuration
class MaConfig(Configuration):
    nom = TextField()
    age = IntegerField()
 
conf = Maconfig('/path/to/config/file')

Et que ce soit le premier truc dans la doc, facile à trouver, facile à comprendre. Que les personnes qui ne veulent pas s’embrouiller avec les détails s’y dessus puissent les ignorer. Sinon Max ne l’utilisera pas :-p

Ils se rendent pas compte

On se rend pas compte à quel point un truc aussi simple que la gestion d’une configuration puisse comporter autant de pièges et de possibilités. Ça me rappelle mon premier rapport de stage à l’université, où j’avais présenté mon grand projet: une classe de logging pour PHP.

A la fin de la présentation, la prof me demande:

– Mais ça sert à quoi votre truc à part à écrire du texte ?
– Ben, c’est le but d’écrire du texte, c’est une classe de logging.
– Mais le minimum que vous pouvez faire avec c’est quoi ?
– Heu… $log->info(‘Foo’)
– Ah, c’est tout ?

Oui connasse, c’est tout. J’ai passé 20 minutes à t’expliquer que je gérais plusieurs niveaux de verbosité, de type de messages, de formating d’output (incluant la stacktrace), qu’il y avait un fichier et une API de config, un système de locking sur le fichier de sortie, et une génération dynamique de la fonction de message pour éviter les blocs conditionnels et blocker le moins de temps possible lors de l’écriture. Ah oui, y a des tests unitaires, et les commentaires permettent aussi de générer la documentation avec Doxygen.

Mais oui c’est tout, je suis juste en seconde année, et toi t’as pas programmé pour vivre depuis près de 10 ans.

Aujourd’hui il y a des libs de logging en PHP bien meilleures que la mienne, mais mon travail est toujours massivement en prod. Je comprends que personne ne passe trop de temps sur ce genre de libs, c’est beaucoup de boulot, et c’est très ingrat.

J’imagine même pas les programmeurs du noyaux linux. Un scheduler ? Ca fait quoi ? – Ben ça permet de déterminer quand un programme à le droit à du temps CPU. – Quoi, c’est tout ? Ca fait deux ans que tu bosses sur un patch d’a peine 2000 lignes juste pour ça ? Eh ben, ça valait le coup de faire un doctorat en théorie des graphes tient !

25 thoughts on “Votre avis sur ce projet de lib de gestion de configuration Python ?

  • Teocali

    Ca me rapelle mon premier rapport de stage à l’université

    Et là, tout est dit…

    Oui, j’ai pas mal de préjugés sur les profs d’université.

    Sinon, pour répondre a ta question, je n’ai pas d’avis sur la question, je ne code pas en Python. Mais je garde un oeil sur ton projet (si jamais ça en devient un), ça m’a l’air interessant.

  • 0kso

    Chouette idée de vouloir unifier tous ces systèmes de configuration !

    J’ai juste envie de revenir sur l’exemple de code dans la section “rester simple”. Et je ne comprends pas en quoi la définition des attributs de la classe MaConfig est nécessaire.

    En effet, un petit coup de __getattr__ permet d’obtenir le nom de l’attribut recherché, et d’aller le chercher dans la config, où qu’elle se trouve. De même pour donner une valeur à un attribut.

  • Sam Post author

    La définition est là pour déclarer plusieurs choses:

    – quel champ est un attribut;
    – le type de chaque attribut;
    – l’existence d’un attribut et sa valeur par défaut;
    – potentiels checks et limites (optionnel).

    Le premier est utile pour les formats de type settings.py dans lesquels il y a du code et des variables intermédiaires parmi les attributs. Dans le cas de Django, les attributs sont en majuscules, mais ce ne sera pas forcément le cas. Dans le XML, le format n’est pas du tout standardisé. Identifier explicitement un attribut permet de ne charger que ce qui est nécessaire et évite la pollution de l’espace de nom: debug plus facile, completion du shell efficace, pas de problème de choix de variables, etc.

    Le deuxième permet le casting. Dans un format comme le CSV, il n’y a pas de type. Le définir permet donc de faire de l’autocasting et éviter de devoir le faire à la main dans le code métier. Liste, dates et même classe custo sont donc ainsi sérialisables de manière transparente. C’est DRY, et tout est centralisé dans un style déclaratif.

    Le troisième permet non seulement de déclencher une erreur si le fichier de configuration est incomplet ou corrompu, mais il permet aussi de retourner une valeur par défaut si l’attribut n’est pas présent dans le fichier de settings, évitant encore une nouvelle fois de surcharger le code métier. C’est aussi très utile pour la première génération du fichier de configuration: si on ne lit pas un fichier (ou autre source d’ailleurs), mais qu’on sauvegarde la configuration pour la première fois, c’est le seul moyen de savoir ce que l’on doit sauvegarder, et ce que l’on attend dans le fichier.

    Le quatrième est optionnel mais toujours utile pour faciliter le debug: écrire une mauvaise valeur dans un fichier de configuration est toujours un problème difficile à détecter. Avoir des checks permet de rapidement identifier le problème: éditer un fichier à la main et faire une faute est si vite arrivé. Un message de type “foo doit être un entier supérieur à 5” est toujours vachement pratique.

    D’ailleurs ça me donne une idée. On devrait avoir une fonction qui versionne les différentes versions de la config silencieusement: si c’est modifié et que ça foire, on peut retourner à l’ancienne version, mais aussi voir l’historique des versions, etc. On pourrait stocker ça de manière invisible dans git, voire carrément utiliser git comme possible source de configuration.

    Mais oui, cela dit il est tout à faire possible d’imaginer que par défaut le bon parser se charge automatiquement en fonction de l’URI de la source de config, et que l’on charge toutes les valeurs avec des castings raisonnables par défaut. Après tout la plupart des script n’ont pas besoin d’un truc très précis.

    On pourrait même imaginer un truc mi-molette, qui utilise des défauts sains, sauf si on a un attribut field qui dit quoi faire. Semi automation.

  • 0kso

    En fait, je verrais plutôt la classe MaConfig comme une définition accessoire des contraintes de la config.

    from configlib import Configuration, Schema
     
    # Accès libre à la config, sans contrainte:
    config1 = Configuration('/path/to/config/file.json')
    print config.url
    config.answer = 42
     
    # Accès avec contrainte:
    class MaContrainte(Constraint):
        nom = TextField()
        age = IntegerField()
     
    config2 = Configuration('http://sametmax.com/config.iso', constraint=MaContrainte())
    config2.nom = 'Bob'
    # Les suivants nous enverrons bouler:
    config2.nom = 34
    config2.surnom = 'Marley'
    config2.age = 3.2

    Qu’en pensez-vous ?

    PS: Bravo pour le coup du trombone ;-)

  • Encolpe DEGOUTE

    C’est un vaste sujet.

    Globalement je suis contre l’utilisation de Python pour la gestion de configuration car il est facile de créer des backdoor lors de l’utilisation d’eval. Pour éviter ce problème cela nécessite d’évaluer la configuration par petits morceaux pour identifier la plus petite structure évaluable possible… bref faire deux fois le boulot.

    J’utilise un système assez simple : est-ce que ce système de gestion de configuration ne gérera que du Python sans interface graphique ?
    Si oui, le format INI est suffisant (comme pour zc.buildout).
    Sinon, j’utilise du XML (ZCML par exemple ou un format normé selon l’application). L’inconvénient de ce deuxième choix est qu’il revient vite à créer un métasytème de configuration dans le genre de maven.

    Plusieurs personnes que j’ai croisé à l’AFPYCamp étaient d’accord sur un point : actuellement zc.buildout convient mais il est trop compliqué (lire mal documenté) et trop lent à faire évoluer. Vous êtes prêts à lancer un projet vous aller vite avoir du monde avec vous, pressé de pouvoir se passer de cet outil.

  • Sam Post author

    @Encolpe DEGOUTE: Je suis plutôt de l’école inverse: j’adore les .py comme fichier de conf pour leur flexibilité et leur lisibilité (et je pense que si ça pose des soucis de sécurité avec, le problème vient de l’input, la politique de sécu, ou l’admin système). Mais je comprends parfaitement qu’on ne soit pas à l’aise avec l’idée.

    Pour le deuxième point:

    Je veux bien croire que le monde de zope soit en galère avec la complexité de leurs outils, mais je ne suis pas sur de comprendre comment le projet pourrait les aider avec buildout ? La difficulté de buildout ne tient-elle pas plutôt de la complexité des recettes et l’absence de doc ? Ou alors il y a vraiment un manque côté parsing de conf ? C’est vrai que ZCML, c’est dur le matin au réveil.

  • Benoît HERVIER

    Intéréssant, mais cela me rappel furieusement la class QSetting du framework Qt.

    * La conf est stocké la ou elle devrait l’être:
    ** base de registre sur windows
    ** ~/.config sur Kde
    ** gconf sur Gnome

    * Signal et Slot de Qt
    * Hierarchie
    * Extensible

    Cependant, cela à l’inconvenient de nécessiter Qt et des binding python (PySide ou PyQt). Mais développant des applications graphique, c c’est massivement ce que j’utilise, un vrai bonheur.

  • Encolpe DEGOUTE

    Il n’y a pas que le monde Zope qui est à la recherche d’outils de gestion de configuration. Chaque logiciel écrit en Python fini par recréer son propre outil de gestion de configuration.
    La difficulté de buildout vient principalement du fait de la mauvaise doc de certaines recette mais surtout de la vision ‘doctest’ de la documentation globale : la plupart des exemples restent obscurs et abscons. Ils sont sans réelle utilité dans les cas de la vie de tous les jours. Il y a aussi quelques petits bugs de parsing que Jim Fulton n’a jamais voulu corriger.
    De son côté, ZCML est tellement bien documenté qu’au final personne ne l’utilise et la plupart des développeurs préfèrent créer des constantes qui sont patchées au démarrage… du coup un collectif a créé grok qui permet de gérer la configuration en Python par dessus le ZCML.
    Tout ceci met en avant tes objectifs : faire simple et si possible réutiliser un système existant (gconf, configparser, etc)

  • Luigi

    J’aime vraiment, mais vraiment beaucoup la fin :)
    Pour le reste, ça vole trop haut pour moi (enfin, je comprends le concept, mais je suis un peu étonné que cela n’existe pas déjà).

  • roro

    Salut, ça fait plaisir de voir qu’il y a encore des cerveaux qui fonctionnent dans ce pays.
    Sam, tu ferait un bon prof. Ton idée: Belle au départ… Chiadée à réaliser…Hum! Au pelage, voila un bel animal…le tout est de le faire entrer dans la cage, sans prendre un coup de dent…
    Qui ne s’attache au détail…Perd l’essentiel (Lao tse revisité Lol)

  • Laurent

    Dafuq did I just read ?
    Ça fait 2 articles que je ne comprends pas j’espère que le prochain parlera de cul.
    (ou du déploiement d’un projet Django)

  • Sam Post author

    @encolpe: ah, c’est ça grok ? J’avais pas pigé. Ok, ça a du sens.

    @roro: je suis prof :-) Je suis dev de métier, mais je complète mes missions de dev avec des missions de formateurs Python, Django et Git. J’ai trop souffert des mauvais profs, c’est thérapeuthique pour moi de faire les cours tels que je les vois. Je peux enseigner Django avec de images de cul, Python avec le cours de la cocaine. C’est tellement plus fun !

    @Laurent: le déploiement Django à l’air de faire l’unanimité, donc il est dans les cartons. Mais c’est long, donc c’est pas pour cette semaine.

  • hdsdi3g

    Question de neophyte en Python : personne n’a déjà fait des fonctions de Serialisation/JavaBean en Python ? Un truc basé sur de l’introspection d’une classe pour geler l’état d’un objet quelconque ?

  • Sam Post author

    Généralement on serialize en pickle (automatique) ou en JSON (manuel) en Python.

    Après, certains projets ont des sérialiseurs automatiques multiformats pour des classes métiers particulières. Par exemple, ce qu’il y a de plus proche de Java Bean sont les sérialiseurs de models de Django:

    du projet django lui-même;
    – ou d’apps de création d’API comme tastypie.

    Et là on gère du json, xml, yaml, html, etc.

  • 0kso

    Par un hazard exceptionnel, il se fait que je viens de commencer un projet qui sera fort axé sur des fichiers de configuration.

    Avez-vous commencé quelque-chose ? Y a-t’il un repository pour pouvoir collaborer ?

  • Sam Post author

    On a rien commencé non. Pour collaborer, rien de tel que github.

  • Sam Post author

    En attendant que tu ai ajouté “issues” dans la config du repo github, ça serait pas mal que tu nous parles de ton projet. Voir si ça colle.

    Au passage, quelqu’un connait un moyen d’installer python-gconf dans un virtualenv ? Ce truc semble vraiment mal foutu.

    EDIT: arf, la mauvaise langue que je suis. J’ai juste pas réalisé que maintenant –no-site–package était remplacé par –system-site-packages.

  • 0kso

    Le projet pour lequel j’aimerais utiliser un système de configuration avancé s’appelle TuxCargo. C’est une idée qui a muri un moment et que je viens de commencer avec deux potes.

  • Sam Post author

    Oula :-) Si tu veux utiliser le projet pour gérer les fichiers de configuration de /etc, y a effectivement beaucoup de boulot.

  • 0kso

    Faut pas abuser non-plus, ce ne sont que quelques fichiers de config qui m’intéressent vraiment :
    – la config de mon programme
    – /etc/network/interfaces
    – /etc/hosts
    – /etc/nginx/sites-enabled/*
    – /var/lib/lxc/{container_name}/config

    Le reste des fonctionnalités pouvant utiliser les outils du système ou des copies de fichiers.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.