Python, configuration applicative : variables d'environnement, fichiers ini et YAML

Introduction

Les fonctionnalités essentielles à aborder lorsqu'on souhaite apprendre et utiliser très rapidement Python :

Bien entendu, aucune valeur de configuration applicative codée en dur dans des programmes, programmes Python ou non.

La configuration peut être extraite à partir :

  • de variables d'environnement
  • de fichiers Ini
  • de fichiers JSON
  • de fichiers XML
  • de fichiers YAML

Dans ce chapître, comment lire (écrire) des données de configuration avec Python à partir de variables d'environnement, de fichiers INI avec configparser et de fichiers YAML avec le package PyYAML.

XML n'est pas abordé ici. Le format XML format est de moins en moins utilisé de nos jours, JSON et YAML ont des formats plus lisibles humainement, de plus les parsers XML sont un peu lourds.

Le format JSON n'est pas couvert également dans ce billet, un article dédié est publié sur ce sujet : Python, lire et écrire des données JSON avec le package natif json

Variables d'environnement, module os

sqlpac@vpsfrsqlpac2$ export CFG=/home/sqlpac/cfg
sqlpac@vpsfrsqlpac2$ echo $CFG
/home/sqlpac/cfg

Pour lire les variables d'environnement, importer le module os et appeler la méthode getenv ou la méthode get de la classe environ :

import os

confdir = os.getenv('CFG')
homedir = os.environ.get('HOME')

print(confdir)
print(homedir)
/home/sqlpac/cfg
/home/sqlpac

Lorsque la variable d'environnement n'existe pas : la méthode getenv et environ.get retournent None.

La variable d'environment peut être extraite directement avec la syntaxe os.environ["Environment Variable"] sans utiliser les méthodes get.

import os
confdir = os.environ['CFG']

Mais l'exception KeyError doit être gérée dans le cas où la variable d'environnement n'existe pas au lieu de tester si None est retourné lors de l'utilisation des méthodes get

import os

varenv = 'CFG2'

try:
	confdir = os.environ[varenv]
except KeyError:
	print('Environment variable %s does not exist' % (varenv))
Environment variable CFG2 does not exist

Le plus important : comment définir une variable d'environnement disponible et accessible dans les sous programmes (shell, Python …) ?

Il suffit de définir la variable d'environnement os.environ['var'] dans le programme parent, la variable d'environnement est alors disponible dans les sous programmes.

subprogram.py
import os
import sys

print('%s : %s' % (sys.argv[0], os.getenv('VERSION')))
build.bash
#!/bin/bash
echo $0" : "$VERSION
import os

os.environ['VERSION']='4.2'

os.system('python3 subprogram.py')
os.system('./build.bash')
Python subprogram.py : 4.2
Shell ./build.bash : 4.2

Fichiers Ini, module configparser

Lire un fichier INI

Un fichier INI exemple (les sections imbriquées ne peuvent pas y être implémentées) :

sqlpac.ini
[sqlpac]

version=5.8
verbosity=2
debug=false
user=sqlpac

wwwurl=https://www.sqlpac.com/
rpc=https://www.sqlpac.com/rpc-secure/

[referential]

dir=https://www.sqlpac.com/referentiel/docs

[googleindexing]

apikey=ApIkeYdGF_kBtPVdAwIM7F0Fu87qWMoykfyl9hfnG2
jsonfile=google-auth-indexing.json

scopes=https://www.googleapis.com/auth/indexing
	
notification=https://indexing.googleapis.com/v3/urlNotifications/metadata?url=
publish=https://indexing.googleapis.com/v3/urlNotifications:publish
		
[mobiletest]
serviceurl=https://searchconsole.googleapis.com/v1/urlTestingTools/mobileFriendlyTest:run

Utiliser le package configparser pour lire un fichier INI. Un objet est créé avec configparser.ConfigParser() et sa méthode read est appelée avec le chemin du fichier ini en argument :

import configparser

cfg = configparser.ConfigParser()
cfg.read('sqlpac.ini')

La méthode sections renvoie les sections dans un objet list :

import configparser

cfg = configparser.ConfigParser()
cfg.read('sqlpac.ini')

print(cfg.sections())
['sqlpac', 'referential', 'googleindexing', 'mobiletest']

Les variables sont extraites avec la syntaxe usuelle :

print(cfg['sqlpac']['debug'])
print(cfg['sqlpac']['version'])

for key in cfg['googleindexing']:
	print(cfg['googleindexing'][key])

false
5.8
ApIkeYdGF_kBtPVdAwIM7F0Fu87qWMoykfyl9hfnG2
google-auth-indexing.json
https://www.googleapis.com/auth/indexing
https://indexing.googleapis.com/v3/urlNotifications/metadata?url=
https://indexing.googleapis.com/v3/urlNotifications:publish

L'objet Config ne devine pas les types de données, le type string est appliqué. Pour convertir vers le type de données correct, utiliser la méthode get appropriée :

version = cfg['sqlpac'].getfloat('version')
verbosity = cfg['sqlpac'].getint('verbosity')
debug = cfg['sqlpac'].getboolean('debug')

Comme avec un dictionnaire, utiliser les méthodes get pour indiquer les valeurs par défaut en cas d'échec (fallback) lorsque la clé n'existe pas :

debug = cfg['sqlpac'].getboolean('debug', False)

Utilisation de variables, interpolations

Pour éviter la redondance, des variables peuvent être définies dans des fichiers INI :

[sqlpac]

wwwurl=https://www.sqlpac.com
rpc=%(wwwurl)s/rpc-secure

Par défaut, l'interpolation basique (basic interpolation) est activée dans ConfigParser. %(var)s est évaluée à la demande où var est définie dans la même section, il n'y a pas besoin de définir et utiliser les variables dans un ordre spécifique.

print(cfg['sqlpac']['rpc'])
https://www.sqlpac.com/rpc-secure

L'interpolation basique évalue les variables pour les directives dans une même section. Quand l'évaluation est nécessaire inter sections ("cross" sections), l'interpolation étendue doit être définie dans l'objet ConfigParser. Dans les interpolations étendues, les variables ont la nomenclature ${section:var} et lorsque la section n'est pas indiquée, la section de la variable est utilisée.

[sqlpac]

wwwurl=https://www.sqlpac.com
rpc=${wwwurl}/rpc-secure
           
[referential]
dir=${sqlpac:wwwurl}/referentiel/docs
import configparser
from configparser import ExtendedInterpolation

cfg = configparser.ConfigParser(interpolation=ExtendedInterpolation())
cfg.read('sqlpac.ini')

print(cfg['sqlpac']['rpc'])
print(cfg['referential']['dir'])
https://www.sqlpac.com/rpc-secure
https://www.sqlpac.com/referentiel/docs

Les interpolations basique et étendue sont exclusives. Il n'est pas possible d'utiliser les 2 à la fois.

Avec respectivement l'interpolation basique et étendue, doubler le caractère % et $ pour échapper le caractère si il est utilisé dans les valeurs des directives de configuration, sinon ils sont candidats à l'interpolation.


notif=50%% done   # % added to bypass basic interpolation
price=$$10        # $ added to bypass extended interpolation

Délimiteurs, commentairess

Les valeurs par défaut pour les délimiteurs et commentaires sont les suivants :

delimiters=('=', ':')
comments=('#', ';')

La première occurence dans une ligne est considérée comme le marqueur de délimiteur ou de commentaire dans la ligne en question.

Bien entendu, ils sont modifiables lors de la création de l'objet ConfigParser :

cfg = configparser.ConfigParser(interpolation=ExtendedInterpolation(),
                                delimiters=('=', ':', '~'))

Écrire un fichier INI

Beaucoup moins utilisé, mais bon à savoir, pour écrire un fichier INI à partir d'un objet dictionnaire, utiliser la méthode write :

import configparser

config = configparser.ConfigParser()

config['sqlpac'] = {}
config['sqlpac']['wwwurl'] = 'https://www.sqlpac.com'
config['sqlpac']['rpc'] = '${wwwurl}/rpc-secure'

with open('sqlpac2.ini', 'w') as cfgfile:
  config.write(cfgfile)
sqlpac2.ini
[sqlpac]
wwwurl = https://www.sqlpac.com
rpc = ${wwwurl}/rpc-secure

Un autre codage en utilisant les méthodes add_section et set :

import configparser

config = configparser.ConfigParser()

config.add_section('sqlpac');
config.set('sqlpac','wwwurl','https://www.sqlpac.com')
config.set('sqlpac','rpc','${wwwurl}/rpc-secure')

with open('sqlpac2.ini', 'w') as cfgfile:
  config.write(cfgfile)

YAML

YAML - Ain't Markup Language : ce n'est pas un langage de balisage. Il est encore plus lisible humainement que le format JSON.

Translation du fichier INI au format YAML

Écrivons le précédent fichier sqlpac.ini au format YAML :

sqlpac.yaml
sqlpac:
  version: 5.8
  verbosity: 2
  debug: false
  user: sqlpac
  wwwurl: https://www.sqlpac.com
  rpc: https://www.sqlpac.com/rpc-secure
  
referential:
  dir: https://www.sqlpac.com/referentiel/docs
  
googleindexing:
  apikey: ApIkeYdGF_kBtPVdAwIM7F0Fu87qWMoykfyl9hfnG2
  jsonfile: google-auth-indexing.json

  scopes: https://www.googleapis.com/auth/indexing
  
  rooturl: https://indexing.googleapis.com/v3
  
  endpoints:
    notification: https://indexing.googleapis.com/v3/urlNotifications/metadata?url=
    publish: https://indexing.googleapis.com/v3/urlNotifications:publish

mobiletest:
  serviceurl: https://searchconsole.googleapis.com/v1/urlTestingTools/mobileFriendlyTest:run

YAML est très intéressant, on peut introduire la sous section endpoints, ce qui n'était pas possible dans le fichier INI.

Mais la mauvaise nouvelle : les variables ne sont plus possibles comme cela a été fait dans le fichier ini avec les interfaces d'interpolation de l'objet ConfigParser.

Dans les spécifications YAML, la définition de variables n'est pas possible. Des ancres (Anchors) peuvent être définies mais elles ne sont utilisables que pour dupliquer des valeurs, la concaténation est interdite, la syntaxe ci-dessous génère une erreur :


sqlpac:
  wwwurl: &url https://www.sqlpac.com
  rpc: *url/rpc-secure

Installation de pyYAML

Le parser YAML n'est pas natif avec Python, un package optionnel doit être installé. Si il n'est pas déjà installé, installer le package PyYAML :

pip3 search PyYAML
PyYAML (5.3.1)                 - YAML parser and emitter for Python
pip3 install PyYAML
Successfully built PyYAML
Installing collected packages: PyYAML
Successfully installed PyYAML-5.3.1

Chargement des fichiers YAML

Pour charger un fichier YAML, importer le module yaml et appeler la méthode load :

import yaml

with open("sqlpac.yaml", "r") as ymlfile:
    cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)

print(cfg["googleindexing"]["endpoints"])
print(cfg["googleindexing"]["endpoints"]["notification"])
{'notification': 'https://indexing.googleapis.com/v3/urlNotifications/metadata?url=', 'publish': 'https://indexing.googleapis.com/v3/urlNotifications:publish'}
https://indexing.googleapis.com/v3/urlNotifications/metadata?url=

Depuis la version 5.1, la méthode load doit être appelée avec l'option Loader, sinon un avertissement est levé :

YAMLLoadWarning: calling yaml.load() without Loader=... is deprecated, as the default Loader is unsafe.
Please read https://msg.pyyaml.org/load for full details.

Les valeurs possibles pour l'option Loader sont :

  • BaseLoader : charge uniquement le format YAML le plus basique.
  • SafeLoader : charge un sous ensemble du langage YAML sécurisé. Recommandé pour le chargement de données non fiables.
  • FullLoader : charge le langage YAML complet mais évite l'exécution de code arbitraire.
  • UnsafeLoader : le chargeur originel qui pouvait être exploité par des données en entrée non fiables.

Qu'en est-il des types de données ? Le type de données approprié est appliqué lors du chargement, aucune conversion nécessaire comparativement aux fichiers INI et configparser :

import yaml

with open("sqlpac.yaml", "r") as ymlfile:
    cfg = yaml.load(ymlfile, Loader=yaml.FullLoader)

for key in ('version','verbosity','debug'):
	print('%s : %s, %s' % (key, cfg["sqlpac"][key], type(cfg["sqlpac"][key])))
version : 5.8, <class 'float'>
verbosity : 2, <class 'int'>
debug : False, <class 'bool'>

Le type de données peut être forcé dans le fichier YAML, par exemple si on veut la directive version en type string et non float :

sqlpac:
  version: !!str 5.8
…
version : 5.8, <class 'str'>

Écrire des fichiers YAML

Pour écrire un fichier YAML, construire un dictionnaire et utiliser la méthode dump :

import yaml
cfgyaml = {}

cfgyaml["sqlpac"] = {}
cfgyaml["sqlpac"]["user"] = "sqlpac"
cfgyaml["sqlpac"]["wwwurl"] = "https://www.sqlpac.com"
cfgyaml["google"] = {}
cfgyaml["google"]["apis"] = ["googleindexing", "googleanalytics"]

with open("sqlpac2.yaml", "w") as f:
	yaml.dump(cfgyaml, f, sort_keys=False)
sqlpac2.yaml
sqlpac:
  user: sqlpac
  wwwurl: https://www.sqlpac.com
google:
  apis:
  - googleindexing
  - googleanalytics

L'option sort_keys dans la méthode dump est disponible uniquement à partir de PyYAML 5.1 livré en Mars 2019, par défaut les clés sont triés.

Conclusion : INI ou YAML pour le fichier de configuration ?

Quand des listes, dictionnaires imbriqués sont intensivement utilisés dans la configuration : YAML est le plus adapté, mais impossible d'utiliser des variables.

De plus YAML est indépendant d'un langage de programmation si il doit être échangé avec d'autres plateformes.

Le fichier INI avec configparser est le meilleur choix quand des variables sont nécessaires, mais il faut dans ce cas gérer les conversions et le fichier INI devient dépendant du langage Python et de la plateforme.