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") |