IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction à Python 3 et au framework web Flask par l'exemple


précédentsommairesuivant

15. Exercice d'application - version 4

Image non disponible

Nous reprenons ici l'exercice décrit au paragraphe Version 3 et nous le traitons maintenant avec des classes et interfaces. Nous écrirons deux applications :

L’application 1 sera la suivante :

Image non disponible

Un script principal [main] instanciera une couche [dao] et une couche [métier] :

  • la couche [dao] aura pour rôle de gérer des données stockées dans des fichiers texte et ultérieurement dans une base de données ;

  • la couche [métier] aura pour rôle de faire le calcul de l'impôt ;

Dans cette application, il n'y aura pas d'actions de la part d'un utilisateur : les données des contribuables seront trouvées dans un fichier texte dont on donnera le nom au module [main].

Dans l’application 2, c'est l'utilisateur qui tapera au clavier les données des contribuables. L'architecture évoluera alors de la façon suivante :

Image non disponible
  • la couche [dao] (Data Access Object) s'occupe de l'accès aux données externes

  • la couche [métier] s'occupe des problèmes métier, ici le calcul de l'impôt. Elle ne s'occupe pas des données. Celles-ci peuvent avoir deux provenances :

    • la couche [dao] pour les données persistantes ;

    • la couche [ui] pour les données fournies par l'utilisateur.

  • la couche [ui] (User Interface) s'occupe des interactions avec l'utilisateur ;

  • [main] est le chef d'orchestre ;

Dans la suite, les couches [dao], [métier] et [ui] seront chacune implémentée à l'aide d'une classe. Les couches [métier] et [dao] seront les mêmes pour les deux applications. C’est la raison pour laquelle on les a réunies dans une même version de l’exercice d’application.

15-1. Version 4 – application 1

La version 4 calcule l'impôt d'une liste de contribuables placée dans un fichier texte. Elle a l'architecture suivante :

Image non disponible

15-1-1. Les entités

Image non disponible

Les entités sont des classes de données. Leur rôle est d’encapsuler des données et d’offrir des getters / setters qui permettent de vérifier la validité de celles-ci. Les entités sont échangées par les couches. Une même entité peut partir de la couche [ui] pour aller jusqu’à la couche [dao] et vice-versa.

15-1-1-1. La classe [ImpôtsError]

Nous utiliserons une classe d'exception propriétaire :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
# -------------------------------
# classe d'exception
from MyException import MyException


class ImpôtsError(MyException):
    pass

Dès que les couches [métier] et [dao] rencontreront un problème, elles lanceront cette exception. Elle dérive de la classe [MyException]. Elle s’utilise donc de la façon suivante : [raise ImpôtsError(code_erreur, msg_erreur)].

15-1-1-2. La classe [AdminData]

La classe [AdminData] encapsule les constantes intervenant dans le calcul de l’impôt :

 
Sélectionnez
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.
from BaseEntity import BaseEntity


# données de l'administration fiscale
class AdminData(BaseEntity):
    # clés exclues de l'état de la classe
    excluded_keys = []

    # clés aurorisées
    @staticmethod
    def get_allowed_keys() -> list:
        return [
            "limites",
            "coeffr",
            "coeffn",
            "plafond_qf_demi_part",
            "plafond_revenus_celibataire_pour_reduction",
            "plafond_revenus_couple_pour_reduction",
            "valeur_reduc_demi_part",
            "plafond_decote_celibataire",
            "plafond_decote_couple",
            "plafond_impot_couple_pour_decote",
            "plafond_impot_celibataire_pour_decote",
            "abattement_dixpourcent_max",
            "abattement_dixpourcent_min"
        ]
  • ligne 5 : la classe [AdminData] étend la classe [BaseEntity] décrite au paragraphe BaseEntity. On se rappelle que les classes étendant la classe [BaseEntity] doivent définir :

    • un attribut de classe [excluded_keys] (ligne 7) qui liste les propriétés de l’objet exclues lorsque l’objet est transformé en dictionnaire ;

    • une méthode statique [get_allowed_keys] (lignes 10-26) qui rend la liste des propriétés acceptées lorsque l’objet est initialisé avec un dictionnaire ;

On n’a pas utilisé de setters pour vérifier la validité des données utilisées pour initialiser un objet [AdminData]. En effet, cet objet est unique et défini par configuration et donc pas susceptible d’être erroné.

15-1-1-3. La classe [TaxPayer]

La classe [TaxPayer] modélisera un contribuable :

 
Sélectionnez
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.
157.
158.
159.
160.
161.
# imports
from BaseEntity import BaseEntity
from ImpôtsError import ImpôtsError


# un contribuable
class TaxPayer(BaseEntity):
    # modélise un contribuable
    # id : identifiant
    # marié : oui / non
    # enfants : son nombre d'enfants
    # salaire : son salaire annuel
    # impôt : montant de l'impôt à payer
    # surcôte : surcôte d'impôt à payer
    # décôte : décôte de l'impôt à payer
    # réduction : réduction sur l'impôt à payer
    # taux : taux d'imposition du contribuable

    # clés exclues de l'état de la classe
    excluded_keys = []

    # clés aurorisées
    @staticmethod
    def get_allowed_keys() -> list:
        return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']

    # properties
    @property
    def marié(self) -> str:
        return self.__marié

    @property
    def enfants(self) -> int:
        return self.__enfants

    @property
    def salaire(self) -> int:
        return self.__salaire

    @property
    def impôt(self) -> int:
        return self.__impôt

    @property
    def surcôte(self) -> int:
        return self.__surcôte

    @property
    def décôte(self) -> int:
        return self.__décôte

    @property
    def réduction(self) -> int:
        return self.__réduction

    @property
    def taux(self) -> float:
        return self.__taux

    # setters
    @marié.setter
    def marié(self, marié: str):
        ok = isinstance(marié, str)
        if ok:
            marié = marié.strip().lower()
            ok = marié == "oui" or marié == "non"
        if ok:
            self.__marié = marié
        else:
            raise ImpôtsError(31, f"l'attribut marié [{marié}] doit avoir l'une des valeurs oui / non")

    @enfants.setter
    def enfants(self, enfants):
        # enfants doit être un entier >=0
        try:
            enfants = int(enfants)
            erreur = enfants < 0
        except:
            erreur = True
        if not erreur:
            self.__enfants = enfants
        else:
            raise ImpôtsError(32, f"L'attribut enfants [{enfants}] doit être un entier >=0")

    @salaire.setter
    def salaire(self, salaire):
        # salaire doit être un entier >=0
        try:
            salaire = int(salaire)
            erreur = salaire < 0
        except:
            erreur = True
        if not erreur:
            self.__salaire = salaire
        else:
            raise ImpôtsError(33, f"L'attribut salaire [{salaire}] doit être un entier >=0")

    @impôt.setter
    def impôt(self, impôt):
        # impôt doit être un entier >=0
        try:
            impôt = int(impôt)
            erreur = impôt < 0
        except:
            erreur = True
        if not erreur:
            self.__impôt = impôt
        else:
            raise ImpôtsError(34, f"L'attribut impôt [{impôt}] doit être un nombre >=0")

    @décôte.setter
    def décôte(self, décôte):
        # décôte doit être un entier >=0
        try:
            décôte = int(décôte)
            erreur = décôte < 0
        except:
            erreur = True
        if not erreur:
            self.__décôte = décôte
        else:
            raise ImpôtsError(35, f"L'attribut décôte [{décôte}] doit être un nombre >=0")

    @surcôte.setter
    def surcôte(self, surcôte):
        # surcôte doit être un entier >=0
        try:
            surcôte = int(surcôte)
            erreur = surcôte < 0
        except:
            erreur = True
        if not erreur:
            self.__surcôte = surcôte
        else:
            raise ImpôtsError(36, f"L'attribut surcôte [{surcôte}] doit être un nombre >=0")

    @réduction.setter
    def réduction(self, réduction):
        # surcôte doit être un entier >=0
        try:
            réduction = int(réduction)
            erreur = réduction < 0
        except:
            erreur = True
        if not erreur:
            self.__réduction = réduction
        else:
            raise ImpôtsError(37, f"L'attribut réduction [{réduction}] doit être un nombre >=0")

    @taux.setter
    def taux(self, taux):
        # taux doit être un réel >=0
        try:
            taux = float(taux)
            erreur = taux < 0
        except:
            erreur = True
        if not erreur:
            self.__taux = taux
        else:
            raise ImpôtsError(38, f"L'attribut taux [{taux}] doit être un nombre >=0")

Notes :

  • la classe [TaxPayer] encapsule un contribuable ;

  • ligne 7 : la classe [TaxPayer] dérive de la classe [BaseEntity]. Elle a donc un identifiant [id] ;

  • ligne 20 : aucune propriété n’est exclue de l’état d’un objet [AdminData] ;

  • lignes 22-25 : les propriétés de la classe. Celles-ci sont explicitées aux lignes 9-17 ;

  • lignes 27-58 : getters des attributs de la classe ;

  • lignes 60-161 : les setters des attributs de la classe. On rappelle que l’intérêt d’une classe encapsulant des données vis-à-vis d’un simple dictionnaire est que la classe peut vérifier la validité de ses propriétés grâce à ses setters ;

15-1-2. La couche [dao]

Image non disponible

Nous allons réunir les implémentations des couches dans un dossier [services]. Ces classes implémenteront des interfaces définies dans le dossier [interfaces].

Image non disponible
15-1-2-1. L'interface [InterfaceImpôtsDao]

La couche [dao] implémentera l'interface [InterfaceImpôtsDao] suivante (fichier InterfaceImpôtsDao.py) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
# imports
from abc import ABC, abstractmethod


# interface IImpôtsDao
from AdminData import AdminData


class InterfaceImpôtsDao(ABC):
    # liste des tranches de l'impôt
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

    # liste des données contribuables
    @abstractmethod
    def get_taxpayers_data(self) -> dict:
        pass

    # écriture des résultats du calcul de l'impôt
    @abstractmethod
    def write_taxpayers_results(self, taxpayers_results: list):
        pass

L'interface définit trois méthodes :

  • [get_admindata] : est la méthode qui obtient le tableau des tranches d'impôt. On notera qu'on ne donne aucun renseignement sur la façon d'obtenir ces données. Dans la suite, elles seront trouvées d'abord dans un fichier texte puis dans une base de données. Ce seront aux classes qui implémentent l'interface de s'adapter au mode de stockage des données. On aura donc une classe pour récupérer les tranches d'impôt dans un fichier texte et une autre pour les récupérer dans une base de données. Elles implémenteront toutes deux la méthode [get_admindata] ;

  • [get_taxpayers_data] : est la méthode qui obtient les données des contribuables. Là encore nous ne disons pas où elles seront trouvées. Nous ne traiterons que le cas où elles sont dans un fichier texte ;

  • [write_taxpayers_results] : est la méthode qui va persister les résultats du calcul de l'impôt. Nous ne disons pas où. Nous ne traiterons que le cas où les résultats sont persistés dans un fichier texte. Le paramètre [taxpayers_results] sera la liste des résultats à persister ;

15-1-2-2. La classe [AbstractImpôtsDao]

La couche [dao] va être implémentée par deux classes :

  • l'une ira chercher les données (contribuables, résultats, tranches d'impôt) dans des fichiers texte ;

  • l'autre ira chercher les données (contribuables, résultats) dans des fichiers texte et les tranches de l'impôt dans une base de données ;

Les deux classes ne vont différer que par la gestion des tranches de l'impôt. Les données contribuables et les résultats des calculs de l'impôt seront, elles, gérées de la même façon. Pour cette raison, nous allons les gérer dans une classe parent [AbstractImpôtsDao]. La particularité de la gestion des tranches d'impôt sera, elle, gérée dans deux classes filles :

  • la classe [ImpôtsDaoWithAdminDataInJsonFile] ira chercher les tranches de l'impôt dans un fichier texte au format jSON ;

  • la classe [ImpôtsDaoWithAdminDataInDatabase] ira chercher les tranches de l'impôt dans une base de données ;

La classe parent [AbstractImpôtsDao] sera la suivante :

 
Sélectionnez
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.
# imports
import codecs
import json
from abc import abstractmethod

from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDao import InterfaceImpôtsDao
from TaxPayer import TaxPayer


# classe de base pour la couche [dao]
class AbstractImpôtsDao(InterfaceImpôtsDao):
    # les contribuables et leur impôt seront dans des fichiers texte
    # constructeur
    def __init__(self, config: dict):
        # config[taxpayersFilename] : le nom du fichier texte des contribuables
        # config[resultsFilename] : le nom du fichier jSON des résultats
        # config[errorsFilename] : le nom du fichier des erreurs

        # on mémorise les paramètres
        self.taxpayers_filename = config.get("taxpayersFilename")
        self.taxpayers_results_filename = config.get("resultsFilename")
        self.errors_filename = config.get("errorsFilename")

    # ------------------
    # interface IImpôtsDao
    # ------------------

    # liste des données contribuables
    def get_taxpayers_data(self) -> dict:
        …

    # écriture de l'impôt des contribuables
    def write_taxpayers_results(self, taxpayers: list):
        …

    # lecture des tranches de l'impôt
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass
  • ligne 13 : la classe [AbstractImpôtsDao] implémente l'interface [InterfaceImpôtsDao]. Aussi trouve-t-on les trois méthodes de cette interface :

    • [get_taxpayers_data] : ligne 31 ;

    • [write_taxpayers_results] : ligne 35 ;

    • [get_admindata] : ligne 40. Cette méthode ne sera pas implémentée par la classe [AbstractImpôtsDao] aussi est-elle déclarée abstraite (ligne 39) ;

  • ligne 16 : le constructeur reçoit un dictionnaire [config] contenant les informations suivantes :

    • [taxpayersFilename] : le nom du fichier texte qui contient les données des contribuables ;

    • [resultsFilename] : le nom du fichier texte dans lequel seront mis les résultats ;

    • [errorsFilename] : le nom du fichier texte listant les erreurs rencontrées lors de l’exploitation du fichier [taxpayersFilename] ;

La méthode [get_taxpayers_data] est la suivante :

 
Sélectionnez
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.
    # liste des données contribuables
    def get_taxpayers_data(self) -> dict:
        # initialisations
        taxpayers_data = []
        datafile = None
        erreurs = []
        try:
            # ouverture du fichier des données
            datafile = open(self.taxpayers_filename, "r")
            # on exploite la ligne courante du fichier
            ligne = datafile.readline()
            # n° de ligne
            numligne = 0
            while ligne != '':
                # une ligne de +
                numligne += 1
                # on enlève les blancs
                ligne = ligne.strip()
                # on ignore les lignes vides et les commentaires
                if ligne != "" and ligne[0] != "#":
                    try:
                        # on récupère les 4 champs id,marié,enfants,salaire qui forment la ligne contribuable
                        (id, marié, enfants, salaire) = ligne.split(",")
                        # on crée un nouveau TaxPayer
                        taxpayers_data.append(
                            TaxPayer().fromdict({'id': id, 'marié': marié, 'enfants': enfants, 'salaire': salaire}))
                    except BaseException as erreur:
                        # on note l'erreur
                        erreurs.append(f"Ligne {numligne}, {erreur}")
                # on lit une nouvelle ligne contribuable
                ligne = datafile.readline()
            # on enregistre les erreurs s'il y en a
            if erreurs:
                text = f"Analyse du fichier {self.taxpayers_filename}\n\n" + "\n".join(erreurs)
                with codecs.open(self.errors_filename, "w", "utf-8") as fd:
                    fd.write(text)
            # on rend le résultat
            return {"taxpayers": taxpayers_data, "erreurs": erreurs}
        except BaseException as erreur:
            # on lance une exception ImpôtsError
            raise ImpôtsError(11, f"{erreur}")
        finally:
            # on ferme le fichier
            if datafile:
                datafile.close()
  • ligne 4 : les données des contribuables (marié, enfants, salaire) seront placées dans une liste d'objets de type [TaxPayer] ;

  • lignes 8-9 : on ouvre le fichier texte des contribuables en lecture. Son contenu a la forme suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

Par rapport aux versions précédentes :

  • chaque ligne du fichier [taxpayersFilename] commence par l'identifiant du contribuable, un simple numéro ;

  • les commentaires et les lignes vides sont autorisés ;

  • on va gérer les erreurs. Ainsi les lignes 17, 19 et 21 doivent être déclarées erronées. Les erreurs sont consignées dans un fichier à part ;

Continuons l’examen du code :

  • ligne 4 : les données du fichier texte sont transférées dans la liste [taxPayersData] ;

  • lignes 14-31 : le fichier des contribuables est lu ligne par ligne ;

  • ligne 14 : la fin du fichier est atteinte lorsqu’on lit une ligne vide (rien – pas même la marque de fin de ligne \r\n) ;

  • ligne 20 : on ignore les lignes vides et les commentaires. Une ligne est un commentaire si, une fois la ligne débarrassée de ses blancs devant et derrière le texte, le 1er caractère est le caractère # ;

  • ligne 24 : une ligne correcte est composée de quatre champs séparés par une virgule. On récupère ceux-ci. L’affectation de données à un tuple de quatre éléments échoue s’il n’y a pas exactement quatre données affectées ;

  • ligne 25 : si l’un des quatre champs récupérés [id, marié, enfants, salaire] est invalide alors la méthode [BaseEntity.fromdict] lancera une exception de type [MyException] ;

  • lignes 25-26 : un objet [TaxPayer] est ajouté à la liste [taxpayers_data] des contribuables ;

  • lignes 27-29 : les éventuelles erreurs sont cumulées dans une liste [erreurs]. Cette liste a été créée ligne 6 ;

  • lignes 33-36 : la liste des erreurs rencontrées est enregistrée dans le fichier texte [errorsFilename]. Elles sont de deux types :

    • une ligne n’avait pas le nombre correct de champs attendus ;

    • les informations de la ligne étaient erronées et ont échoué à construire un objet [TaxPayer] ;

  • lignes 39-41 : on intercepte toute erreur (BaseException) et on la remonte en l'encapsulant dans un type [ImpôtsError] ;

  • lignes 42-45 : dans tous les cas, réussite ou échec, le fichier texte des contribuables est fermé s'il a été ouvert ;

La méthode [write_taxpayers_results] doit produire un fichier jSON de la forme :

 
Sélectionnez
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.
[
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 2,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },
  {
    "id": 3,
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000,
    "impôt": 0,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 720,
    "réduction": 0
  },]

La méthode [write_taxpayers_results] est la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
    # écriture de l'impôt des contribuables
    def write_taxpayers_results(self, taxpayers: list):
        # écriture des résultats dans un fichier jSON
        # taxpayers : liste d'objets de type TaxPayer
        # (id, marié, enfants, salaire, impôt, surcôte, décôte, réduction, taux)
        # la liste [taxpayers] est enregistrée dans le fichier texte [self.taxpayers_results_filename]
        file = None
        try:
            # ouverture du fichier des résultats
            file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
            # création de la liste à sérialiser en jSON
            mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
            # sérialisation jSON
            json.dump(list(mapping), file, ensure_ascii=False)
        except BaseException as erreur:
            # on relance l'erreur sous un autre type
            raise ImpôtsError(12, f"{erreur}")
        finally:
            # on ferme le fichier s'il a été ouvert
            if file:
                file.close()
  • ligne 2 : la méthode reçoit une liste de contribuables [taxpayers] qu'elle doit enregistrer dans le fichier texte [self.taxpayers_results_filename] au format jSON ;

  • ligne 10 : création du fichier UTF-8 des résultats ;

  • ligne 12 : nous introduisons ici la fonction [map] dont la syntaxe ici est [map (fonction, liste1)]. La fonction [fonction] est appliquée à chaque élément de [liste1] et produit un nouvel élément qui alimente une liste [liste2]. Finalement, pour chaque i :

     
    Sélectionnez
    1.
    liste2[i]=fonction(liste1[i])
    

    Ici, [liste1] est la liste [taxPayers], une liste d'objets de type [TaxPayer]. La fonction [fonction] est exprimée ici sous la forme d'une fonction dite [lambda] qui exprime la transformation faite sur un élément [taxpayer] de la liste [taxpayers] : chaque élément [taxpayer] est remplacé par son dictionnaire [taxpayer.asdict()]. Finalement, la liste [liste2] obtenue est la liste des dictionnaires des éléments de la liste [taxpayers] ;

  • ligne 12 : le résultat rendu par la fonction [map] n'est pas la liste [liste2] mais un objet de type [map]. Pour avoir [liste2], il faut utiliser l'expression [list(mapping)] (ligne 14) ;

  • ligne 14 : la liste [liste2] est enregistrée au format jSON dans le fichier [self.taxpayers_results_filename] ;

  • lignes 15-17 : tout type d'exception est intercepté et encapsulé dans une erreur de type [ImpôtsError] avant d'être relancé (ligne 17) ;

  • lignes 19-21 : dans tous les cas, réussite ou échec, le fichier des résultats est fermé s'il a été ouvert ;

15-1-2-3. Classe [ImpôtsDaoWithAdminDataInJsonFile]

La classe [ImpôtsDaoWithAdminDataInJsonFile] va dériver de la classe [AbstractImpôtsDao] et implémenter la méthode [getAdminData] que sa classe parent n'a pas implémentée. Elle ira chercher les données de l'administration fiscale dans un fichier jSON :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

La classe [ImpôtsDaoWithAdminDataInJsonFile] est la suivante :

 
Sélectionnez
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.
# imports
import codecs
import json

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError


# une implémentation de la couche [dao] où les données de l'administration fiscale sont dans un fichier jSON
class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
    # constructeur
    def __init__(self, config: dict):
        # config[admindataFilename] : le nom du fichier jSON contenant les données de l'administration fiscale
        # config[taxpayersFilename] : le nom du fichier texte des contribuables
        # config[resultsFilename] : le nom du fichier jSON des résultats
        # config[errorsFilename] : le nom du fichier des erreurs

        # initialisation de la classe Parent
        AbstractImpôtsDao.__init__(self, config)
        # lecture des données de l'administration fiscale
        file = None
        try:
            # ouverture du fichier jSON des données fiscales en lecture
            file = codecs.open(config["admindataFilename"], "r", "utf8")
            # transfert du contenu du fichier jSON dans un objet [AdminData]
            self.admindata = AdminData().fromdict(json.load(file))
        except BaseException as erreur:
            # on relance l'erreur sous la forme d'un type [ImpôtsError]
            raise ImpôtsError(21, f"{erreur}")
        finally:
            # fermeture du fichier s'il a été ouvert
            if file:
                file.close()

    # -------------
    # interface
    # -------------

    # récupération des données de l'administration fiscale
    # la méthode rend un objet [AdminData]
    def get_admindata(self) -> AdminData:
        return self.admindata
  • ligne 11 : la classe [ImpôtsDaoWithAdminDataInJsonFile] hérite de la classe [AbstractImpôtsDao]. A ce titre elle implémente l'interface [InterfaceImpôtsDao] ;

  • ligne 13 : le constructeur reçoit en paramètre un dictionnaire contenant les informations des lignes 14-17 ;

  • ligne 20 : la classe parent est initialisée ;

  • ligne 24 : ouverture du fichier jSON des données de l'administration fiscale ;

  • ligne 25 : le fichier UTF-8 des données de l’administration fiscale est ouvert ;

  • ligne 27 : le contenu du fichier est lu et placé dans l’objet [self.admindata] de type [AdminData]. Il faut que les clés du fichier jSON correspondent aux propriétés acceptées pour un objet [AdminData] sinon la méthode [fromdict] lancera une exception ;

  • lignes 28-30 : gestion des exceptions. Les exceptions qui peuvent se produire sont encapsulées dans un type [ImpôtsError] avant d'être relancées ;

  • lignes 32-34 : le fichier est fermé s'il a été ouvert ;

  • lignes 42-43 : implémentation de la méthode [get_admindata] de l'interface [InterfaceImpôtsDao] ;

15-1-3. La couche [métier]

Image non disponible
15-1-3-1. L'interface [InterfaceImpôtsMétier]

L'interface de la couche [métier] sera la suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
# imports
from abc import ABC, abstractmethod

from AdminData import AdminData
from TaxPayer import TaxPayer


# interface IImpôtsMétier
class InterfaceImpôtsMétier(ABC):
    # calcul de l'impôt pour 1 contribuable
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        pass
  • l'interface [InterfaceImpôtsMétier] définit une unique méthode :

    • ligne 12 : la méthode [calculate_tax] permet de calculer l'impôt d'un unique contribuable [taxpayer]. [admindata] est l’objet [AdminData] encapsulant les données de l'administration fiscale ;

    • ligne 12 : la méthode [calculate_tax] ne rend pas de résultat. Les données obtenues (impôt, surcôte, décôte, réduction, taux) sont incluses dans le paramètre [taxpayer] : avant l'appel ces attributs sont vides, après l'appel ils ont été initialisés ;

15-1-3-2. La classe [ImpôtsMétier]

La classe [ImpôtsMétier] implémente l'interface [InterfaceImpôtsMétier] de la façon suivante :

Image non disponible

Les méthodes de la classe sont issues du module [impôts_module_02] du paragraphe Le module [impots.v02.modules.impôts_module_02]. On a seulement limité les paramètres des méthodes à deux :

  • taxpayer(id, marié, enfants, salaire, impôt, décôte, surcôte, réduction, taux) : l'objet représentant un contribuable et son impôt ;

  • admindata : l’objet encapsulant les données de l'administration fiscale ;

Nous montrons sur une méthode les changements ainsi apportés ;

 
Sélectionnez
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.
    # calcul de l'impôt - phase 1
    # ----------------------------------------
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
        # taxpayer(id, marié, enfants, salaire, impôt, décôte, surcôte, réduction, taux)
        # admindata : données de l'administration fiscale

        # calcul de l'impôt avec enfants
        self.calculate_tax_2(taxpayer, admindata)
        # les résultats sont dans taxpayer
        taux1 = taxpayer.taux
        surcôte1 = taxpayer.surcôte
        impot1 = taxpayer.impôt
        # calcul de l'impôt sans les enfants
        if taxpayer.enfants != 0:
            # calcul de l'impôt pour le même contribuable sans enfants
            taxpayer2 = TaxPayer().fromdict(
                {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
            self.calculate_tax_2(taxpayer2, admindata)
            # les résultats sont dans taxpayer2
            taux2 = taxpayer2.taux
            surcôte2 = taxpayer2.surcôte
            impot2 = taxpayer2.impôt
            # application du plafonnement du quotient familial
            if taxpayer.enfants < 3:
                # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
                impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
            else:
                # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
                impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
                         * 2 * admindata.plafond_qf_demi_part
        else:
            # si le contribuable n'a pas d'enfants alors impot2=impot1
            impot2 = impot1

        # on prend l'impôt le plus fort avec le taux et la surcôte qui vont avec
        (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
            impot2, impot2 - impot1 + surcôte2, taux2)

        # résultats partiels
        taxpayer.impôt = impot
        taxpayer.surcôte = surcôte
        taxpayer.taux = taux
        # calcul d'une éventuelle décôte
        self.get_décôte(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.décôte
        # calcul d'une éventuelle réduction d'impôts
        self.get_réduction(taxpayer, admindata)
        taxpayer.impôt -= taxpayer.réduction
        # résultat
        taxpayer.impôt = math.floor(taxpayer.impôt)
  • ligne 3: la méthode [calculate_tax] est l'unique méthode de l'interface [InterfaceImpôtsMétier]. Elle admet deux paramètres :

    • [tapPayer] : le contribuable dont on calcule l'impôt ;

    • [admindata] : l’objet encapsulant les données de l'administration fiscale ;

    • les résultats du calcul sont encapsulés dans le paramètre [taxpayer] (lignes 40-50). Le contenu de cet objet n'est donc pas le même avant et après l'appel à la méthode ;

15-1-4. Tests des couches [dao] et [métier]

Image non disponible
  • [TestDaoMétier] est la classe UnitTest de test des couches [dao] et [métier] ;

  • [config] est le fichier de configuration des tests ;

La configuration [config] est la suivante :

 
Sélectionnez
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.
def configure():
    import os

    # étape 1 ------
    # configuration du python Path

    # dossier de ce fichier
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # dépendances absolues de l'application
    absolute_dependencies = [
        f"{script_dir}/../entities",
        f"{script_dir}/../interfaces",
        f"{script_dir}/../services",
        f"{root_dir}/02/entities",
    ]

    # on fixe le syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # étape 2 ------
    # configuration de l'application
    config = {
        # chemins absolus des fichiers de l'application
        "admindataFilename"f"{script_dir}/../data/input/admindata.json"
    }

    # instanciation des couches de l'application
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # on met les instances de couches dans la config
    config["dao"= dao
    config["métier"= métier

    # on rend la config
    return config
  • lignes 4-23 : on configure le Python Path des tests ;

  • lignes 32-41 : on instancie les couches [dao] et [métier]. On met leurs références dans le dictionnaire [config] ;

  • ligne 44 : on rend ce dictionnaire ;

La classe de test [TestDaoMétier] est la suivante :

 
Sélectionnez
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.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
import unittest


def get_config() -> dict:
    # configuration de l'application
    import config
    # on rend la configuration
    return config.configure()


class TestDaoMétier(unittest.TestCase):

    # exécutée avant chaque méthode test_
    def setUp(self) -> None:
        # on récupère la configuration des tests
        config = get_config()
        # on mémorise quelques informations
        self.métier = config['métier']
        self.admindata = config['dao'].get_admindata()

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
        # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérification
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_2(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 2, 'salaire': 50000,
        # 'impôt': 1384, 'surcôte': 0, 'décôte': 384, 'réduction': 347, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 1384, delta=1)
        self.assertAlmostEqual(taxpayer.décôte, 384, delta=1)
        self.assertAlmostEqual(taxpayer.réduction, 347, delta=1)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_3(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 3, 'salaire': 50000,
        # 'impôt': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertEqual(taxpayer.impôt, 0)
        self.assertAlmostEqual(taxpayer.décôte, 720, delta=1)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_4(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'non', 'enfants': 2, 'salaire': 100000,
        # 'impôt': 19884, 'surcôte': 4480, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 19884, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 4480, delta=1)

    def test_5(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'non', 'enfants': 3, 'salaire': 100000,
        # 'impôt': 16782, 'surcôte': 7176, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 16782, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7176, delta=1)

    def test_6(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 3, 'salaire': 100000,
        # 'impôt': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction': 0, 'taux': 0.3}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 9200, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.3, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 2180, delta=1)

    def test_7(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 5, 'salaire': 100000,
        # 'impôt': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 4230, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_8(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'non', 'enfants': 0, 'salaire': 100000,
        # 'impôt': 22986, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 22986, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_9(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 2, 'salaire': 30000,
        # 'impôt': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertEqual(taxpayer.impôt, 0)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.0, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    def test_10(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'non', 'enfants': 0, 'salaire': 200000,
        # 'impôt': 64210, 'surcôte': 7498, 'décôte': 0, 'réduction': 0, 'taux': 0.45}
        taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 64210, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.45, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 7498, delta=1)

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        # {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
        # 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        self.métier.calculate_tax(taxpayer, self.admindata)
        # vérifications
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':
    unittest.main()

Commentaires

  • ligne 11 : la classe de test étend la classe [unittest.TestCase] ;

  • lignes 13-19 : dans un test UnitTest, la méthode [setUp] est exécutée avant chacune des méthodes [test_] ;

  • ligne 16 : on récupère la configuration issue du script [config] étudié précédemment ;

  • ligne 18 : on mémorise une référence sur la couche [métier] ;

  • ligne 19 : on demande à la couche [dao] l’objet [AdminData] encapsulant les données de l’administration fiscale et on le mémorise ;

  • lignes 21-173 : 11 tests dont les résultats ont été vérifiés sur le site officiel des impôts 2019 https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm ;

  • lignes 21-33 : tous les tests ont été construits sur le même modèle ;

  • ligne 22 : on importe la classe [TaxPayer] ;

  • ligne 24 : contribuable testé ;

  • ligne 25 : résultats attendus ;

  • ligne 26 : construction de l’objet [TaxPayer] du contribuable ;

  • ligne 27 : calcul de son impôt. Le résultat est dans [taxpayer] ;

  • lignes 29-33 : vérification des résultats obtenus ;

  • ligne 29 : on vérifie le montant de l’impôt à l’euro près. Les tests ont en effet montré que les résultats obtenus par l’algorithme de ce document pouvaient différer des chiffres officiels au plus d’un montant de 1 euro ;

L’exécution des tests donne les résultats suivants :

Image non disponible
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
Testing started at 16:08 ...
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\plugins\python-ce\helpers\pycharm\_jb_unittest_runner.py" --path C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py
Launching unittests with arguments python -m unittest C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/tests/TestDaoMétier.py in C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\tests



Ran 11 tests in 0.055s

OK

Process finished with exit code 0

15-1-5. Script principal

Image non disponible

Le script principal est configuré par le script [config] suivant :

 
Sélectionnez
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.
def configure():
    import os

    # étape 1 ------
    # configuration du python Path

    # dossier de ce fichier
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # dépendances de l'application
    absolute_dependencies = [
        # dépendances locales
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # on fixe le syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # étape 2 ------
    # configuration de l'application
    config = {
        # chemins absolus des fichiers de l'application
        "taxpayersFilename"f"{script_dir}/../../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../../data/output/résultats.json",
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
        "errorsFilename"f"{script_dir}/../../data/output/errors.txt"
    }

    # instanciation des couches de l'application
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier

    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    métier = ImpôtsMétier()

    # on met les instances de couches dans la config
    config["dao"= dao
    config["métier"= métier

    # on rend la config
    return config

Il est analogue à celui utilisé pour le test des couches [métier] et [dao].

Le script principal [main.py] est le suivant :

 
Sélectionnez
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.
# on configure l'application
import config

config = config.configure()

# imports
from ImpôtsError import ImpôtsError

# on récupère les couches de l'application (elles sont déjà instanciées)
dao = config["dao"]
métier = config["métier"]

try:
    # récupération des tranches de l'impôt
    admindata = dao.get_admindata()
    # lecture des données des contribuables
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # des contribuables ?
    if not taxpayers:
        raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # calcul de l'impôt des contribuables
    for taxpayer in taxpayers:
        # taxpayer est à la fois un paramètre d'entrée et de sortie
        # taxpayer va être modifié
        métier.calculate_tax(taxpayer, admindata)
    # écriture des résultats dans un fichier texte
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # affichage de l'erreur
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # terminé
    print("Travail terminé...")

Notes

  • lignes 2-4 : on récupère la configuration de l’application. On sait également que le Python Path de l’application a été construit ;

  • lignes 9-11 : on récupère des références sur les couches [métier] et [dao] ;

  • ligne 15 : on obtient les données de l'administration fiscale ;

  • ligne 17 : on obtient la liste des contribuables dont il faut calculer l'impôt ;

  • lignes 19-20 : si cette liste est vide, on lève une exception ;

  • lignes 22-25 : calcul de l'impôt des différents objets [taxpayer] grâce à la couche [métier] ;

  • ligne 27 : [taxpayers] est désormais une liste d'objets [TaxPayer] où les attributs (impôt, décôte, surcôte, réduction, taux) ont reçu une valeur. Cette liste est écrite dans un fichier jSON ;

  • lignes 28-30 : interception d'une éventuelle erreur ;

  • lignes 31-33 : exécutées dans tous les cas ;

L’exécution du script donne les mêmes résultats que dans les versions précédentes. Le fichier des erreurs de contribuables était une nouveauté dans cette version. Après exécution du script [main] son contenu est le suivant :

 
Sélectionnez
1.
2.
3.
4.
5.
Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v04\main\01/../../data/input/taxpayersdata.txt

Ligne 17, not enough values to unpack (expected 4, got 2)
Ligne 19, too many values to unpack (expected 4)
Ligne 21, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]

Les lignes erronées étaient les suivantes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on peut avoir des lignes vides

# on crée des lignes erronées
# pas assez de valeurs
11,12
# trop de valeurs
12,oui,3,200000, x, y
# des valeurs erronées
x,x,x,x

15-2. Version 4 – application 2

Dans cette version, c'est l'utilisateur au clavier qui donne la liste des contribuables. L'architecture de l'application sera la suivante :

Image non disponible

Un nouveau module apparaît : la couche [ui] (User Interface) qui va dialoguer avec l'utilisateur. Cette couche aura une interface et sera implémentée par une classe.

Image non disponible

15-2-1. L'interface [InterfaceImpôtsUi]

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
# imports
from abc import ABC, abstractmethod


# interface InterfaceImpôtsUI
class InterfaceImpôtsUi(ABC):
    # exécution de la classe implémentant l'interface
    @abstractmethod
    def run(self):
        pass

L'interface [InterfaceImpôtsUi] n'aura qu'une méthode, celle des lignes 8-10. L'interface sera ici implémentée avec une application console mais on pourrait aussi l'implémenter avec une interface graphique. Les paramètres passés à la méthode [run] ne seraient pas les mêmes dans les deux implémentations. Pour contourner ce problème, la méthode habituelle est de :

  • ne pas mettre de paramètres à la méthode [run] (ou le minimum de paramètres) ;

  • passer des paramètres au constructeur de la classe implémentant l'interface. Ils peuvent être différents d'une implémentation à l'autre. Ces paramètres sont enregistrés comme attributs de la classe ;

  • faire en sorte que la méthode [run] utilise ces attributs de classe (self.x) ;

Cette méthode permet d'avoir une interface très générale qui est précisée par les paramètres des constructeurs de chaque classe d'implémentation. Cette méthode a déjà été utilisée pour la version modulaire n° 1.

15-2-2. La classe [ImpôtsConsole]

La classe [ImpôtsConsole] implémente l'interface [InterfaceImpôtsUi] de la façon suivante :

 
Sélectionnez
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.
# imports
import re

from InterfaceImpôtsUi import InterfaceImpôtsUi
from TaxPayer import TaxPayer


# couche [UI]
class ImpôtsConsole(InterfaceImpôtsUi):
    # constructeur
    def __init__(self, config: dict):
        # on mémorise les paramètres
        self.admindata = config['dao'].get_admindata()
        self.métier = config['métier']

    def run(self):
        # dialogue interactif avec l'utilisateur
        fini = False
        while not fini:
            # le contribuable est-il marié ?
            marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # vérification de la validité de la saisie
            while marié != "oui" and marié != "non" and marié != "*":
                # msg d'erreur
                print("Tapez oui ou non ou *")
                # question de nouveau
                marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
            # fini ?
            if marié == "*":
                # dialogue terminé
                return
            # nombre d'enfants
            enfants = input("Nombre d'enfants : ").strip()
            # vérification de la validité de la saisie
            if not re.match(r"^\d+$", enfants):
                # msg d'erreur
                print("Tapez un nombre entier positif ou nul")
                # on recommence
                enfants = input("Nombre d'enfants : ").strip()
            # salaire annuel
            salaire = input("Salaire annuel : ").strip()
            # vérification de la validité de la saisie
            if not re.match(r"^\d+$", salaire):
                # msg d'erreur
                print("Tapez un nombre entier positif ou nul")
                # on recommence
                salaire = input("Salaire annuel : ").strip()
            # calcul de l'impôt
            taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
            self.métier.calculate_tax(taxpayer, self.admindata)
            # affichage
            print(f"Impôt du contribuable = {taxpayer}\n\n")
            # contribuable suivant
  • ligne 9 : la classe [ImpôtsConsole] implémente l'interface [InterfaceImpôtsUi] ;

  • ligne 11 : le constructeur de la classe reçoit un paramètre, le dictionnaire [config]  de la configuration de l’application ;

    • ligne 13 : on récupère les données de l’administration fiscale permettant le calcul de l’impôt ;

    • ligne 14 : on mémorise une référence sur la couche [métier] ;

  • ligne 16 : implémentation de la méthode [run] de l'interface ;

  • lignes 19-53 : dialogue avec l'utilisateur. Il consiste

    • à demander les trois informations (marié, enfants, salaire) du contribuable ;

    • à calculer son impôt ;

    • à afficher celui-ci ;

    • le dialogue se termine lorsque l'utilisateur répond * à la première question ;

  • lignes 20-27 : on demande si le contribuable est marié et on vérifie la validité de la réponse ;

  • lignes 29-31 : si l’utilisateur a répondu ‘*’ à la question le dialogue est arrêté ;

  • lignes 32-39 : on demande le nombre d'enfants du contribuable et on vérifie la validité de la réponse ;

  • lignes 40-47 : on demande le salaire annuel du contribuable et on vérifie la validité de la réponse ;

  • lignes 48-50 : avec ces informations on fait calculer, par la couche [métier], l'impôt du contribuable ;

  • ligne 52 : le montant de l'impôt est affiché ;

15-2-3. Le script principal

Le script principal [main] est configuré par le fichier [config] suivant :

 
Sélectionnez
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.
def configure():
    import os

    # étape 1 ------
    # configuration du python Path

    # dossier de ce fichier
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"

    # dépendances de l'application
    absolute_dependencies = [
        # dépendances locales
        f"{script_dir}/../../entities",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{root_dir}/02/entities",
    ]

    # on fixe le syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # étape 2 ------
    # configuration de l'application
    config = {
        # chemins absolus des fichiers de l'application
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json",
    }

    # instanciation des couches de l'application
    from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
    from ImpôtsMétier import ImpôtsMétier
    from ImpôtsConsole import ImpôtsConsole

    # couche dao
    dao = ImpôtsDaoWithAdminDataInJsonFile(config)
    # couche métier
    métier = ImpôtsMétier()
    # on met les instances de couches dans la config
    config["dao"= dao
    config["métier"= métier
    # couche ui
    ui = ImpôtsConsole(config)
    config["ui"= ui

    # on rend la config
    return config

Le script chef d'orchestre est le suivant (main.py) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
# on configure l'application
import config

config = config.configure()

# imports
from ImpôtsError import ImpôtsError

# on récupère les couches de l'application (elles sont déjà instanciées)
ui = config["ui"]

# code
try:
    # exécution de la couche [ui]
    ui.run()
except ImpôtsError as erreur:
    # on affiche le message d'erreur
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # exécuté dans tous les cas
    print("Travail terminé...")
  • lignes 1-4 : on récupère la configuration de l’application ;

  • ligne 10 : on récupère une référence sur la couche [ui] ;

  • lignes 12-21 : la structure du code est la même que dans l’application précédente : du code entouré d'un try / catch pour arrêter toute éventuelle exception ;

  • ligne 15 : on demande à la couche [ui] de s'exécuter : le dialogue avec l'utilisateur commence alors ;

  • lignes 16-18 : interception d'une éventuelle exception ;

Voici un exemple d'exécution :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v04/main/02/main.py
Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : oui
Nombre d'enfants : 3
Salaire annuel : 200000
Impôt du contribuable = {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}


Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : *
Travail terminé...

Process finished with exit code 0

précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Serge Tahé et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2020 Developpez.com.