Nécessaires pour la gestion des paramètres
Avant-propos
OrderedDict
"<" | ">" | "=" | "^"
Description
params = {'node5': {'datas': {'name': 'Signaux', 'icon': 'signaux.png'}, 'position': (-64, 160), 'active': 2}, 'UiView': {'zoom': 4, 'position': (-29, -25)}, 'CtrlScene': {"Paramètres du projet 'Projet 03'": {"Nom de l'onglet": 'New-York', 'Grille magnétique': True, 'Couleur de fond': '#393939', 'Couleur des traits': '#484848', 'Couleur des repères': '#2e2e2e'}, 'collapsed': set()}}
params
contient 3 dictionnaires :
params = {'node5': {...}, 'UiView': {...}, 'CtrlScene': {...}}
node5: ------------------------------------------- <class 'dict'> datas: --------------------------------------- <class 'dict'> name: Signaux ---------------------------- <class 'str'> icon: signaux.png ------------------------ <class 'str'> position: (-64, 160) ------------------------- <class 'tuple'> active: 2 ------------------------------------ <class 'int'> UiView: ------------------------------------------ <class 'dict'> zoom: 4 -------------------------------------- <class 'int'> position: (-29, -25) ------------------------- <class 'tuple'> CtrlScene: --------------------------------------- <class 'dict'> Paramètres du projet 'Projet 03': ------------ <class 'dict'> Nom de l'onglet: New-York ---------------- <class 'str'> Grille magnétique: True ------------------ <class 'bool'> Couleur de fond: #393939 ----------------- <class 'str'> Couleur des traits: #484848 -------------- <class 'str'> Couleur des repères: #2e2e2e ------------- <class 'str'> collapsed: set() ----------------------------- <class 'set'>
Dictionary
, qui possède les méthodes suivantes :
print()
, qui affiche dans le terminal le dictionnaire de façon 'humaine', comme ci-dessus.
dashes()
key_list()
: L'exemple ci-dessus est composé de 17 lignes : 5 branches (<class 'dict'>
) et 12 feuilles.
[
['node5', 'datas', 'name'],
['node5', 'datas', 'icon'],
['node5', 'position'],
['node5', 'active'],
['UiView', 'zoom'],
['UiView', 'position'],
['CtrlScene', "Paramètres du projet 'Projet 03'", "Nom de l'onglet"],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Grille magnétique'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur de fond'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur des traits'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur des repères'],
['CtrlScene', 'collapsed']
]
read()
: Voir commentaires dans le code.write()
: Voir commentaires dans le code._sort_cut()
: méthode interne, utilisée par read()
et write()
. Seules les clés à la racine (depth = 0) sont concernées.
node5
, UiView
et CtrlScene
delete()
: Voir commentaires dans le code.utils.py
, par exemple au début, après les imports :
class Dictionary(OrderedDict):
def __init__(self, dic=None, max_keys=0):
"""
:param dic: dict() or None.
:param max_keys: Nombre max de clés (de depth=0) autorisé dans le dictionnaire :
- Si -1 → infini.
- Sinon, gestion de l'ordre des clés lors de l'écriture :
- Les plus récentes en tête.
- Suppression de celles dont l'ordre est > max_keys.
"""
super().__init__()
self.max_keys = max_keys
if isinstance(dic, dict):
""" 'self' est un dictionnaire vide. """
self.update(dic)
def print(self, dic=None, length=60, dash='-', depth=0, blank=3):
"""
- Méthode récursive (qui s'appelle elle-même), d'où la présence du depth.
- Elle affiche plusieurs lignes, chacune se terminant par "<class '...'>"
:param dic: Dictionnaire à afficher.
:param length: Nombre de caractères, ou, plus précisément, position pour chaque ligne du texte "<class '...'>"
:param dash: Symbole '-', '_', '.', ' ', ou tout autre caractère.
:param depth: Le 1er élément a un depth de 0. S'il contient à so tour un dictionnaire,
celui-ci aura un depth de 1, et ainsi de suite.
:param blank: 0, 1, 2 ou 3
0 = pas de ligne vide avant ni après l'affichage du dictionnaire.
1 = Une ligne vide avant l'affichage du dictionnaire.
2 = Une ligne vide après l'affichage du dictionnaire.
3 = Une ligne vide avant ET après l'affichage du dictionnaire (valeur par défaut).
:return: NA, le retour est en fait une succession de print() dans la console 'run'.
En cas d'erreur :
- Si dic vide, afficher 'Dictionnaire vide.'
- Si dic n'est pas un dictionnaire, afficher :
f"Vérifie dico : Le paramètre reçu n'est pas un dictionnaire.\nType : {type(dic)}\nValeur : {dic}"
"""
# à coder ...
print('print') # Provisoire, à supprimer.
@staticmethod
def dashes(before, nb, after, dash='-'):
""" Formatage de chaine : https://pyformat.info/ <-- Ctrl + Clic
Exemple : " position: (-29, -25) ------------------------- <class 'tuple'>"
<------- before -------> <---- nb de tirets ----> <--- after --->
<------- nb = before + 1 + nb de tirets --------->
:param before: Chaîne avant les tirets.
:param nb: taille de before + 1 + Nombre de tirets.
:param after: Chaîne après les tirets.
:param dash: Symbole '-', '_', '.', ' ', ou tout autre caractère.
:return: Chaîne complète, exemple → " position: (-29, -25) ------------------------- <class 'tuple'>"
"""
# à coder ...
print('dashes') # Provisoire, à supprimer.
def key_list(self):
"""
:return: Liste des clés. Chaque clé est elle-même une liste.
"""
def recursive(dic, l_keys):
for key, val in dic.items():
if isinstance(val, dict):
recursive(val, l_keys + [key])
else:
ll_keys.append(l_keys + [key])
ll_keys = list() # ll_ = Liste de listes
recursive(self, [])
return ll_keys
def read(self, l_keys, default=None):
"""
:param l_keys: Si c'est une liste, elle correspond à la hiérarchie dans l'arbre.
:param default: Valeur par défaut à retourner lorsque la recherche échoue.
:return: Valeur de la dernière clé de la liste.
"""
# à coder ...
print('read') # Provisoire, à supprimer.
def write(self, l_keys, value):
"""
:param l_keys: Si c'est une liste, elle correspond à la hiérarchie dans l'arbre.
:param value: Valeur affectée à la dernière clé de la liste -> sans enfant (feuille).
:return: Booléen de réussite.
"""
# à coder ...
print('write') # Provisoire, à supprimer.
def _sort_cut(self, master_key):
""" SORT AND CUT
Méthode appelée lorsqu'on accède au dictionnaire, en lecture comme en écriture.
self.max_keys est le nombre maximum admis de clés de 1er niveau (depth=0).
- Si self.max_keys <= 0 (par défaut) le nombre maximum admis est infini.
- Si self.max_keys > 0, tri en remontant la clé master_key en 1ère place, puis ...
|_ ... supprime les clés de depth 0 dont l'index est supérieur ou égal au max autorisé.
:param master_key: Clé à remonter (de depth 0).
:return: NA
"""
# à coder ...
print('_sort_cut', self.max_keys) # Provisoire, à supprimer.
def delete(self, l_keys):
"""
Suppression, si elle existe, de la clé correspondant à l_keys.
:param l_keys: liste des clés, ordonnées hiérachiquement.
:return: True si la clé a effectivement été supprimée.
False si échec ou si la clé est absente.
"""
# à coder ...
print('delete') # Provisoire, à supprimer.
from collections import OrderedDict
print()
provisoires et les commentaires inutiles.Vérification
utils.py
.Debug 'utils'
.
if __name__ == '__main__':
""" Tous les tests sont enfermés dans des fonctions pour éviter les variables globales ...
... qui provoqueraient des effets de bord. """
def test1():
d_params = {'node5': {'datas': {'name': 'Signaux', 'icon': 'signaux.png'}, 'position': (-64, 160), 'active': 2},
'UiView': {'zoom': 4, 'position': (-29, -25)}, 'CtrlScene': {
"Paramètres du projet 'Projet 03'": {"Nom de l'onglet": 'New-York', 'Grille magnétique': True,
'Couleur de fond': '#393939', 'Couleur des traits': '#484848',
'Couleur des repères': '#2e2e2e'}, 'collapsed': set()}}
dic = Dictionary(d_params)
""" Test manuel de la fonction key_list(). """
ll_keys = dic.key_list()
for l_keys in ll_keys:
print(l_keys)
test1()
test_utils.py
. Lancez les tests après avoir remplacé tout son contenu par celui-ci :
from functions.utils import Utils, Dictionary
from PyQt5.QtCore import QSettings
import pytest
import shutil
import random
import os
import sys
import time
import copy
ut = Utils()
# @pytest.mark.skip # Commenter cette ligne pour exécuter les tests.
class TestUi2py:
@pytest.fixture()
def files(self):
"""
:return (yield): Dictionnaire -> chemins + liste des fichiers-cible.
"""
def clean_targets():
for target in l_targets:
if os.path.isfile(tests_path + target):
os.remove(tests_path + target)
def clean_design():
if os.path.isdir(design_path):
shutil.rmtree(design_path)
tests_path = os.getcwd() + os.sep
pc_path = f"{os.path.dirname(os.getcwd())}{os.sep}pc{os.sep}"
design_path = f"{tests_path}design"
""" Copie du dossier 'pc/design' dans tests/design afin de protéger les sources. """
clean_design() # Suppression d'un éventuel ancien dossier.
shutil.copytree(pc_path + 'design', design_path)
l_targets = list()
for file_name in os.listdir(design_path):
if file_name.lower().endswith('.ui'):
l_targets.append(f"{file_name[:-2]}py")
if file_name.lower().endswith('.qrc'):
l_targets.append(f"{file_name[:-4]}_rc.py")
if len(l_targets) == 0:
print("\nIl n'y a aucun fichier à compiler")
assert len(l_targets) > 0
""" Nettoyage d'éventuels anciens fichiers compilés. """
clean_targets()
yield {
'tests_path': tests_path,
'pc_path': pc_path,
'design_path': design_path,
# 'list_targets': []
'list_targets': l_targets
}
""" Nettoyage. """
clean_targets()
clean_design()
@staticmethod
# @pytest.mark.skip # Commenter cette ligne pour exécuter le test.
def test_ui2py_no(files):
""" Pas de compilation. La méthode doit retourner True. """
assert ut.ui2py(action=False) is True
@staticmethod
# @pytest.mark.skip # Commenter cette ligne pour exécuter le test.
def test_ui2py_yes(files):
"""
Compilation forcée.
Pour chaque fichier_source :
- La 'date-heure' du fichier_cible est égale à {dh_now} (à 500 ms près).
"""
l_targets = files['list_targets']
tests_path = files['tests_path']
""" Compilation inconditionnelle (True = forcée). """
ut.ui2py(action=True)
""" Comparaison des dates-heures des fichiers-cible produits, avec dh actuel (dh_now)
L'écart doit être faible : 500ms max. Disons moins de 2 secondes, par sécurité. """
dh_now = time.time()
for target in l_targets:
ecart = dh_now - os.path.getmtime(tests_path + target)
assert ecart < 2 # Inférieur à 2 secondes.
@staticmethod
# @pytest.mark.skip # Commenter cette ligne pour exécuter le test.
def test_ui2py_auto(files):
"""
Compilation conditionnelle.
Pour chaque fichier_source dont la 'date-heure' a changé :
- La 'date-heure' du fichier_cible est égale à {now} (à quelques ms près).
"""
def get_dh_memo():
lt_dh = list() # Liste de tuples.
for file_name in os.listdir(design_path):
if file_name.lower().endswith('.ui') or file_name.lower().endswith('.qrc'):
_file_py = file_name[:-3] if file_name.lower().endswith('.ui') else file_name[:-4] + '_rc'
_file_source = design_path + os.sep + file_name
_file_target = f"{tests_path}{_file_py}.py"
dh_source_memo = settings.value(f"dh_{_file_source}", 0.) # Valeur par défaut = 0.
lt_dh.append((_file_source, _file_target, dh_source_memo))
return lt_dh
tests_path = files['tests_path'] # D:\Robot\tests\ (avec \)
design_path = files['design_path'] # D:\Robot\tests\design (sans \)
settings = QSettings(f"{tests_path}{os.sep}params.conf", QSettings.IniFormat)
""" Test 1 ****************************************************************************************
Les dh ont changé => Les dh des fichiers-cible doivent changer. """
""" - Préparation : Changement artificiel des dates. """
for t_dh in get_dh_memo():
design_file = t_dh[0]
os.utime(design_file)
""" - Lancement de ui2py() en mode automatique. """
ut.ui2py()
""" - Vérification : => Les dh des fichiers-cible doivent être le dh actuel (à 500ms près). """
for t_dh in get_dh_memo():
file_target = t_dh[1]
if os.path.isfile(file_target):
dh = os.path.getmtime(file_target)
delay = time.time() - dh
assert delay < 2 # 2 secondes, par sécurité.
else:
assert os.path.isfile(file_target) is True
""" Test 2 ****************************************************************************************
Les dh-sources n'ont pas changé => Les dh des fichiers-cible ne doivent pas changer. """
""" - Préparation : Égalisation artificielle des dh_source. """
target_time = time.time() - 1000 # Valeur dans le passé
for t_dh in get_dh_memo():
design_file = t_dh[0]
dh_memo = t_dh[2]
os.utime(design_file, (dh_memo, dh_memo))
""" Modification des dh-cibles avant traitement. """
target_file = t_dh[1]
os.utime(target_file, (target_time, target_time))
""" - Lancement de ui2py() en mode automatique. """
ut.ui2py()
""" - Vérification : Les dh des fichiers-cible n'ont pas changé (elles sont égales à {target_time}) """
for t_dh in get_dh_memo():
target_file = t_dh[1]
dh = os.path.getmtime(target_file)
assert dh == target_time
""" Test 3 ****************************************************************************************
Certains dh-sources ont changé => Les dh des fichiers-cible correspondants doivent changer, pas les autres. """
""" Préparation : Mélange de la liste des fichiers, modification du premier, pas des autres. """
l_dh_memo = get_dh_memo()
random.shuffle(l_dh_memo)
os.utime(l_dh_memo[0][0]) # Un fichier, au hasard.
""" - Lancement de ui2py() en mode automatique. """
ut.ui2py()
""" Vérification : Seul le premier fichier de la liste a été compilé. """
now = time.time()
for i, t_dh in enumerate(l_dh_memo):
target_file = t_dh[1]
dh = os.path.getmtime(target_file)
delay = now - dh
if i == 0:
assert delay < 2 # Le premier a été modifié : 2 secondes, par sécurité.
else:
assert delay > 1000 # Les autres n'ont pas été modifiés.
# @pytest.mark.skip
class TestDict:
class PrintToStr:
""" Émulation de la sortie standard. La méthode 'write()' est nécessaire. """
def __init__(self):
self.txt = ''
def write(self, txt):
""" Le 'print' est remplacé par l'appel à cette méthode. """
self.txt += txt
d_dic = {'node5': {'datas': {'name': 'Signaux', 'icon': 'signaux.png'}, 'position': (-64, 160), 'active': 2},
'UiView': {'zoom': 4, 'position': (-29, -25)},
'CtrlScene': {"Paramètres du projet 'Projet 03'": {
"Nom de l'onglet": 'New-York', 'Grille magnétique': True,
'Couleur de fond': '#393939', 'Couleur des traits': '#484848',
'Couleur des repères': '#2e2e2e'}, 'collapsed': set()}
}
ll_keys = [
['node5', 'datas', 'name'],
['node5', 'datas', 'icon'],
['node5', 'position'],
['node5', 'active'],
['UiView', 'zoom'],
['UiView', 'position'],
['CtrlScene', "Paramètres du projet 'Projet 03'", "Nom de l'onglet"],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Grille magnétique'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur de fond'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur des traits'],
['CtrlScene', "Paramètres du projet 'Projet 03'", 'Couleur des repères'],
['CtrlScene', 'collapsed']
]
l_vals = ['Signaux', 'signaux.png', (-64, 160), 2, 4, (-29, -25), 'New-York', True, '#393939', '#484848',
'#2e2e2e', set()]
# @pytest.mark.skip
def test_dashes(self):
od_dic = Dictionary()
line = od_dic.dashes(' text1', 15, 'text2', '.')
assert line == ' text1 ...... text2'
# @pytest.mark.skip
def test_print(self):
o_print = self.PrintToStr()
sys.stdout = o_print # Détournement de la sortie standard vers l'objet o_print.
# https://www.quennec.fr/book/export/html/679#:~:text=En%20Python%2C%20comme%20en%20Bash,vers%20un%20fichier.&text=Quand%20on%20ex%C3%A9cute%20le%20script,myLogFile%20This%20is%20a%20test.
od_dic = Dictionary(copy.deepcopy(self.d_dic))
od_dic.print(blank=0)
sys.stdout = sys.__stdout__ # Rétablissement de la sortie standard.
l_consigne = """node5: ----------------------------------------------------- <class 'dict'>
datas: ------------------------------------------------- <class 'dict'>
name: Signaux -------------------------------------- <class 'str'>
icon: signaux.png ---------------------------------- <class 'str'>
position: (-64, 160) ----------------------------------- <class 'tuple'>
active: 2 ---------------------------------------------- <class 'int'>
UiView: ---------------------------------------------------- <class 'dict'>
zoom: 4 ------------------------------------------------ <class 'int'>
position: (-29, -25) ----------------------------------- <class 'tuple'>
CtrlScene: ------------------------------------------------- <class 'dict'>
Paramètres du projet 'Projet 03': ---------------------- <class 'dict'>
Nom de l'onglet: New-York -------------------------- <class 'str'>
Grille magnétique: True ---------------------------- <class 'bool'>
Couleur de fond: #393939 --------------------------- <class 'str'>
Couleur des traits: #484848 ------------------------ <class 'str'>
Couleur des repères: #2e2e2e ----------------------- <class 'str'>
collapsed: set() --------------------------------------- <class 'set'>""".split('\n')
l_dashes = o_print.txt.split('\n')[: -1] # [: -1] = Suppression du dernier '\n'.
assert len(l_dashes) == len(l_consigne)
for i in range(len(l_dashes)):
assert l_dashes[i] == l_consigne[i]
# @pytest.mark.skip
def test_key_list(self):
od_dic = Dictionary(copy.deepcopy(self.d_dic))
ll_key_list = od_dic.key_list()
assert len(ll_key_list) == len(self.ll_keys)
for i in range(len(self.ll_keys)):
assert ll_key_list[i] == self.ll_keys[i]
# @pytest.mark.skip
def test_read(self):
od_dic = Dictionary(copy.deepcopy(self.d_dic))
ll_key_list = od_dic.key_list()
for i in range(len(ll_key_list)):
assert od_dic.read(ll_key_list[i]) == self.l_vals[i]
""" Valeurs par défaut. """
assert od_dic.read('Cle inexistante', 'xyz') == 'xyz'
assert od_dic.read(['Cle1', 'Cle2'], 'xyz') == 'xyz'
# @pytest.mark.skip
def test_write(self):
""" Dictionnaire vide. """
od_dic = Dictionary()
""" Construction du dictionnaire ligne par ligne. """
for i in range(len(self.ll_keys)):
od_dic.write(self.ll_keys[i], self.l_vals[i])
nb_lines = len(od_dic.key_list())
assert nb_lines == 12
""" Comparaison ligne par ligne avec la consigne. """
d_consigne = Dictionary(copy.deepcopy(self.d_dic))
for l_keys in self.ll_keys:
assert od_dic.read(l_keys) == d_consigne.read(l_keys)
# @pytest.mark.skip
def test_delete(self):
od_dic = Dictionary(copy.deepcopy(self.d_dic))
od_ref = Dictionary(copy.deepcopy(self.d_dic))
""" Suppression d'une clé, au hasard. """
l_keys = random.choice(self.ll_keys)
b_reussite = od_dic.delete(l_keys)
assert b_reussite is True
""" Tentative de suppression de cette même clé, qui a déjà été supprimée. """
b_reussite = od_dic.delete(l_keys)
assert b_reussite is False
""" Suppression de plusieurs clés, successsivement. """
for i in range(6):
ll_keys = od_dic.key_list()
l_keys = random.choice(ll_keys)
value_yes = od_dic.read(l_keys, 'inexistant') # 'inexistant' = valeur par défaut.
od_dic.delete(l_keys)
value_no = od_dic.read(l_keys, 'inexistant') # 'inexistant' = valeur par défaut.
assert value_yes == od_ref.read(l_keys) # Avant suppression -> Valeur réelle.
assert value_no == 'inexistant' # Après suppression -> inexistant.
""" Nombre de clés restantes : nb initial - 8 """
nb_cles_dic = len(od_dic.key_list())
nb_cles_ref = len(od_ref.key_list())
assert nb_cles_dic == nb_cles_ref - 7
# @pytest.mark.skip
def test_sort_cut(self):
od_dic = Dictionary(copy.deepcopy(self.d_dic), max_keys=2) # 3 clés de depth 0.
""" Nombre de clés à la racine (depth=0). """
nb_keys = len(od_dic.keys())
od_dic.read(['CtrlScene', "Paramètres du projet 'Projet 03'", 'Grille magnétique'], True)
assert len(od_dic.keys()) == nb_keys - 1
""" La clé ancêtre demandée (ici'CtrlScene') est en 1ère position. """
for l_key in od_dic.keys():
assert l_key == 'CtrlScene'
break
""" Création d'une clé, et remontée de celle-ci en 1ère place. """
od_dic.write(['grand_pere', 'pere', 'enfant'], 'Jules')
""" La clé ancêtre demandée (ici'grand_pere') est en 1ère position. """
for l_key in od_dic.keys():
assert l_key == 'grand_pere'
break
Snippets
Bonjour les codeurs !