Le poste de contrôle (PC) / Les paramètres /
Paramètres : 1 - Dictionnaires triés

Nécessaires pour la gestion des paramètres


Avant-propos


Description

Problème

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()}}
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'>

 

A faire : Ajouter le code suivant dans le fichier 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.
Ne pas oublier d'importer la classe OrderedDict
from collections import OrderedDict
A faire : coder aux emplacements 'à coder', retirer les print() provisoires et les commentaires inutiles.

Vérification

Sans TDD : Vous pouvez tester chaque fonction sans TDD, comme ceci :
  • Ajoutez ce code à la fin du fichier utils.py.
  • Placez-y un point d'arrêt.
  • Lancez-le ensuite en pas à pas, en faisant (clic droit) Debug 'utils'.
  • Utilisez les petits boutons de pas à pas, ou mieux, utilisez les raccourcis-clavier.
  • A chaque pas, observez la valeur des variables.
  • Si vous n'êtes pas familer avec le debug, entraînez-vous avec cet exemple.
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()

 

Avec TDD : Le fichier de test est 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

Essayez de résoudre cette fonctionnalité par vous-même.
Consultez les réponses (snippets) seulement si vous n'avez pas trop de temps.

Bonjour les codeurs !