Sam & Max » configuration http://sametmax.com Du code, du cul Sat, 07 Nov 2015 10:56:13 +0000 en-US hourly 1 http://wordpress.org/?v=4.1 Personnalisez le démarrage d’iPython 8 http://sametmax.com/personnalisez-le-demarrage-dipython/ http://sametmax.com/personnalisez-le-demarrage-dipython/#comments Sun, 16 Dec 2012 13:14:54 +0000 http://sametmax.com/?p=3643 iPython, c’est bon. Et ça peut être encore meilleur.

Avant on ajoutait un peu de sel à notre super shell en éditant les fichiers .ipython/ipythonrc et .ipython/ipy_user_conf.py. Mais ça c’était avant.

Maintenant on fait :

$ ipython profile create

Ce qui va créer un fichier ~/.config/ipython/profile_default/ipython_config.py (et ipython_qtconsole_config.py si vous avez installé la version qt).

Il ne vous reste plus qu’à éditer ce fichier pour personnaliser le démarrage de iPython.

Executer du code au démarrage

N’importe quelle ligne de code Python ou de commande magique (les trucs qui commencent pas ‘%’ qui ne fonctionnent que dans iPython) !

Très utile pour les imports par exemple. Voici ce que je fais moi au démarrage :

c.TerminalIPythonApp.exec_lines = [
'%doctest_mode',
'import os, sys, re, json',
'from datetime import datetime, timedelta',
'''
try:
    from path import path
except ImportError:
    pass
''',
'''
try:
    import requests
except ImportError:
    pass
''',
'''
try:
    from batbelt.structs import *
    from batbelt.objects import attr
    from batbelt.strings import slugify, normalize, json_dumps, json_loads
except ImportError:
    pass
'''
]

Du coup j’ai quasiment jamais besoin d’importer un truc, car ce que j’utilise le plus est déjà là. %doctest_mode me permet de coller des docstrings sans me fouler.

Programmation graphique

iPython possède une boucle principale. Les toolkits graphiques aussi. Si vous faites vos essais du code du second dans le premier, ça va bloquer. Heureusement on peut demander à iPython d’intégrer la main loop d’un des toolkits graphiques les plus célèbres en choisissant parmi : ‘qt’, ‘wx’, ‘gtk’, ‘glut’, ‘pyglet’ et ‘osx’ et en settant :

c.TerminalIPythonApp.gui = 'qt'

Virer le header

Quand on lance le shell, il vous raconte sa vie. Pour lui dire de fermer sa gueule :

c.TerminalIPythonApp.display_banner = False

Lancer un script complet

Bon, ça c’est si vous avez de gros besoins…

c.TerminalIPythonApp.file_to_run = '/path/to/script.py'

Sauter la confirmation à la fermeture

Oui, je suis sûr te vouloir te fermer connard ! Tu crois que j’ai appuyé sur Ctrl + d par erreur en éternuant ?

c.TerminalInteractiveShell.confirm_exit = False

Choisir son éditeur externe

Si vous tapez %edit dans iPython, il ouvre un éditeur. Vous tapez votre code, vous sauvegardez, vous fermez. Et iPython récupère le code et l’éxécute. Par défault il utilise VI. On peut faire mieux.

c.TerminalInteractiveShell.editor = '/chemin/vers/sublime-text -w'
]]>
http://sametmax.com/personnalisez-le-demarrage-dipython/feed/ 8
Votre avis sur ce projet de lib de gestion de configuration Python ? 25 http://sametmax.com/votre-avis-sur-ce-projet-de-lib-de-gestion-de-configuration-python/ http://sametmax.com/votre-avis-sur-ce-projet-de-lib-de-gestion-de-configuration-python/#comments Wed, 01 Aug 2012 21:36:27 +0000 http://sametmax.com/?p=1444 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 !

]]>
http://sametmax.com/votre-avis-sur-ce-projet-de-lib-de-gestion-de-configuration-python/feed/ 25