15. Exercice d'application - version 4▲
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](./images/image-139.png)
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](./images/image-140.png)
-
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](./images/image-141.png)
15-1-1. Les entités▲
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 :
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 :
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 :
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](./images/image-143.png)
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].
15-1-2-1. L'interface [InterfaceImpôtsDao]▲
La couche [dao] implémentera l'interface [InterfaceImpôtsDao] suivante (fichier InterfaceImpôtsDao.py) :
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 :
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 :
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 :
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 :
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 :
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électionnez1.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 :
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 :
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](./images/image-145.png)
15-1-3-1. L'interface [InterfaceImpôtsMétier]▲
L'interface de la couche [métier] sera la suivante :
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 :
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 ;
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]▲
-
[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 :
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 :
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 :
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▲
Le script principal est configuré par le script [config] suivant :
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 :
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 :
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 :
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 :
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.
15-2-1. L'interface [InterfaceImpôtsUi]▲
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 :
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 :
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) :
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 :
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