Bonjour

parfois, nos scripts ont besoin d'�tre configurables et existe plusieurs techniques pour passer ces param�tres, mais existe aussi plusieurs niveaux o� nous pouvons passer ces param�tres.


Pour simplifier l'exemple, je vais partir d'un script qui a en configuration une couleur pour mettre en �vidences certains textes.

Le cas probable o� nous pouvons trouver le plus de cas d'utilisation est sans doute un script open-source:
1) Le d�veloppeur d�fini une couleur par d�faut
2) Le packageur va d�finir une autre couleur qui correspond � sa distribution
3) Chaque utilisateur du m�me syst�me peut re-d�finir sa propre couleur (durablement ou ponctuellement)


R�solution du probl�me :

1)
Nous en avons l'habitude, nous �crivons dans le code source la configuration.

2)
Il faut donc sauvegarder dans le syst�me cette configuration. Si il n'existe pas, alors nous le cr�ons avec les r�glages du codeur. Le plus habituel est le fichier de type .ini car il est relativement simple � modifier.
Un fichier .json n'est pas une bonne id�e parce qu'un utilisateur non avanc� a de fortes chances de casser ce format lorsqu'il va �diter ce fichier.
Ce fichier est stock� dans le syst�me avec l'application pour windows et sous linux normalement dans /etc/.
Existe plusieurs biblioth�ques pour lire ce type de fichiers avec python. Note: Le code exemple va utiliser la nouvelle librairie int�gr�e dans python 3.11.

3)
Pour changer durablement au niveau utilisateur, il faut en cons�quence avoir ce m�me fichier dans son espace utilisateur (fichier optionnel).

Et si un utilisateur d�sire un changement temporaire ? Alors, il faut qu'il puisse le passer directement � notre application.
Existe plusieurs fa�ons, mais puisque nous sommes gentils, nous allons en donner la possibilit� � notre utilisateur.
1) il peut passer des param�tres au script (--theme.color=rouge, syntaxe exemple) si c'est un changement ponctuel
2) il peut utiliser des variables d'environnement
3) pour passer beaucoup de param�tres au script, il peut �ventuellement injecter un fichier de configuration dans stdin


Ce cahier des charges correspond � notre demande, une application hautement configurable et � tous niveaux. Et au final, l'utilisateur gagne et a plusieurs choix en fonction de ces attentes.





Chaque technique individuellement est tr�s simple � mettre en �uvre, il faut en fait simplement les combiner.

Dans la classe Contexte()
Code : S�lectionner tout - Visualiser dans une fen�tre � part
self.items = self.items | self.user_load() | self.env_load() | self.stdin_load() | self.params_load()
Cette ligne va d�terminer quelle technique va �craser l'autre. Pour environnement, stdin et param�tres, c'est au codeur de donner une pr�f�rence, d'en supprimer ou m�me d'en ajouter.


Note :
Ce script n'est aucunement une biblioth�que, mais uniquement une compilation de techniques, un chainage "simple" pour toutes les proposer � l'utilisateur final.
Uniquement pour cette d�mo, Le fichier .ini "syst�me" n'est pas dans le syst�me


R�sultat du script :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 
python main.py --theme.color OR --config.debug=5 
Configurations, détail:
 Système:  /home/Data/Patrick/workspace/python/configuration/override.conf/config système test/OverrideConf/config.ini 
    {'theme': {'color': 'red'}, 'config': {'debug': 1, 'lang': 'fr'}}
 
 Utilisateur:  /home/patrick/.config/OverrideConf.ini 
    {'theme': {'color': 'blue'}}
 
 StdIn:
        # `./main.py --config.debug=1 < test.ini`
    {}
 
 Passage de paramètres:
        # `./main.py --theme.color green ou ./main.py --theme.color=green`
    {'theme': {'color': 'OR'}, 'config': {'debug': '5'}}
 
########################
 
Merge les 4: paramètre du script écrase stdIn, stdIn écrase utilisateur et utilisateur écrase système :
{'theme': {'color': 'OR'}, 'config': {'debug': '5'}}
 
Pour notre thème, nous allons utiliser la couleur: OR pour mettre en évidence certains textes
```
Nous pouvons facilement voir, si debug est sup�rieur � 0 :
Que le packageur (ou dev si le fichier n'existait pas) avait choisi la couleur rouge et debug niveau 1.
L'utilisateur "patrick" a par d�faut : couleur bleu.
Mais que pour cette fois, il a la couleur Or et un niveau "debug" �gal � 5.

Code :
Code : S�lectionner tout - Visualiser dans une fen�tre � part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#!/usr/bin/env python
 
import argparse
from pathlib import Path
import sys
import tomllib  # uniquement python 3.11
 
 
PACKAGE = "OverrideConf"    # nom de l'applications
 
 
class Contexte:
    """ on charge une configuration """
 
    SYSTEM_PATH = Path("/etc")      # valeur classique sous linux
    SYSTEM_PATH = Path(__file__).parent / "config système test" # uniquement pour la démo
 
    def __init__(self, /, auto_create=False):
        self.auto_create = auto_create    # éventuellemnt créer une arborescence
        self.items = self.system_load()
        self.items = self.items | self.user_load() | self.env_load() | self.stdin_load() | self.params_load()
 
    @property
    def file_system(self) -> Path:
        return self.SYSTEM_PATH / PACKAGE / "config.ini"
 
    @property
    def file_user(self) -> Path:
        return Path.home() / ".config" / f"{PACKAGE}.ini"
 
    def system_load(self) -> dict:
        """ configuration système dans fichier """
        config_file = self.file_system
        data = None
        if not config_file.exists():
            # CAS REEL : on n'a sens doute pas les droits pour créer le fichier
            config_file.parent.mkdir(parents=True, exist_ok=True)
            config_file.write_text(self._default_datas())
 
        with open(config_file, "rb") as f_conf:
            data = tomllib.load(f_conf)
        return data
 
    def user_load(self) -> dict:
        """ configuration utilisateur dans fichier """
        config_file = self.file_user
        data = {}
        if not config_file.exists():
            config_file.parent.mkdir(parents=True, exist_ok=True)
            config_file.write_text('[theme]\ncolor="blue"')
        with open(config_file, "rb") as f_conf:
            data = tomllib.load(f_conf)
        return data
 
    def params_load(self) -> dict:
        """ quelques variables passées au script """
        # on ne prend que si la variable existe dans fichier .ini
        # A réécrire si on désire plus de 2 niveaux...
        data = {}
        for key, item in self.items.items():
            for subitem in item.keys():
                key_param = f"--{key}.{subitem}"
                for i, param in enumerate(sys.argv):
                    if param.startswith(key_param):
                        if "=" in param:
                            # format: --truc=1
                            data.setdefault(key, {})
                            data[key][subitem] = param.split("=", maxsplit=2)[-1].strip()
                        else:
                            # format: --truc 1
                            try:
                                value = sys.argv[i+1].strip()
                                if not value.startswith("-"):
                                    data.setdefault(key, {})
                                    data[key][subitem] = value
                            except IndexError:
                                pass
 
        return data
 
    def stdin_load(self) -> dict:
        """ un fichier.ini passé en flux d'entrée au script """
        if not sys.stdin.isatty():
            file_data = sys.stdin.read()
            print("->", file_data)
            return tomllib.loads(file_data)
        return {}
 
    def env_load(self) -> dict:
        """ lecture des variables d'environnement"""
        # pratiquement même code que params_load() et même plus simple
        return {}
 
    @staticmethod
    def _default_datas() -> str:
        """ A titre de démo uniquement, il est plus logique d'avoir un dictionnaire par defaut"""
        return '[theme]\ncolor = "red"\n\n[config]\ndebug=1\nlang="fr"\n'
 
    def __call__(self, args: str):
        """ facon `simple` d'accéder aux valeurs """
        args = args.split(".")
        item = self.items
        for arg in args:
            try:
                item = item.get(arg)
            except AttributeError:
                break
        if isinstance(item, str):
            if item.isdigit():
                return int(item)
        return item
 
 
if __name__ == "__main__":
 
    configuration = Contexte(True)
    if configuration("config.debug"):
        # pour la démo
        print("Configurations, détail:")
 
        print(" Système: ", configuration.file_system, "\n   ", configuration.system_load())
        print()
 
        print(" Utilisateur: ", configuration.file_user, "\n   ", configuration.user_load())
        print()
 
        print(" StdIn:")
        print("\t# `./main.py --config.debug=1 < test.ini`")
        print("   ", configuration.stdin_load())
        print()
 
        print(" Passage de paramètres:")
        print("\t# `./main.py --theme.color green ou ./main.py --theme.color=green`")
        print("   ", configuration.params_load())
        print()
 
        print("#"*24)
        print()
        print("Merge les 4: paramètre du script écrase stdIn, stdIn écrase utilisateur et utilisateur écrase système :")
        print(configuration.items)
        print("\n")
 
 
    # nous pouvons aussi avoir des commandes...
    # pour distinction, ici, elles débutent par un seul tiret
    parser = argparse.ArgumentParser(prog=PACKAGE, description='app python hautement configurable')
    parser.add_argument('-a', '-add', type=int)
    parser.add_argument('-r', '-run', action='store_false')
    parser.add_argument('-s', '-supp', action='store_false')
    # eventuellement ajouter --theme.color pour uniquement la documentation et ne pas le traiter ici avec argparse puisque fait précédemment
    args, unknown = parser.parse_known_args()
    if args : print("Commandes à exécuter:", args)
 
    print()
    print()
    print("Pour notre thème, nous allons utiliser la couleur:", configuration("theme.color"), "pour mettre en évidence certains textes")