Start new project
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-20 20:09:48 +02:00
parent c4a7d94926
commit 2fd26218da
7 changed files with 597 additions and 737 deletions

View File

@ -1,290 +1,4 @@
\hypertarget{_sources}{%
\subsubsection{Sources}\label{_sources}}
\begin{itemize}
\item
\href{http://www.codeproject.com/Articles/703634/SOLID-architecture-principles-using-simple-Csharp}{Understanding
SOLID principles on CodeProject}
\item
\href{http://lostechies.com/derickbailey/2011/09/22/dependency-injection-is-not-the-same-as-the-dependency-inversion-principle/}{Dependency
Injection is NOT the same as dependency inversion}
\item
\href{http://en.wikipedia.org/wiki/Dependency_injection}{Injection de
dépendances}
\end{itemize}
\hypertarget{_au_niveau_des_composants_2}{%
\subsection{au niveau des
composants}\label{_au_niveau_des_composants_2}}
De la même manière que pour les principes définis ci-dessus, Mais
toujours en faisant attention qu'une fois que les frontières sont
implémentés, elles sont coûteuses à maintenir. Cependant, il ne s'agit
pas une décision à réaliser une seule fois, puisque cela peut être
réévalué.
Et de la même manière que nous devons délayer au maximum les choix
architecturaux et techniques,
\begin{quote}
but this is not a one time decision. You don't simply decide at the
start of a project which boundaries to implémentent and which to ignore.
Rather, you watch. You pay attention as the system evolves. You note
where boundaries may be required, and then carefully watch for the first
inkling of friction because those boundaries don't exist. at that point,
you weight the costs of implementing those boundaries versus the cost of
ignoring them and you review that decision frequently. Your goal is to
implement the boundaries right at the inflection point where the cost of
implementing becomes less than the cost of ignoring.
\end{quote}
En gros, il faut projeter sur la capacité à s'adapter en minimisant la
maintenance. Le problème est qu'elle ne permettait aucune adaptation, et
qu'à la première demande, l'architecture se plante complètement sans
aucune malléabilité.
\hypertarget{_reuserelease_equivalence_principle}{%
\subsubsection{Reuse/release equivalence
principle}\label{_reuserelease_equivalence_principle}}
\begin{verbatim}
Classes and modules that are grouped together into a component should be releasable together
-- (Chapitre 13, Component Cohesion, page 105)
\end{verbatim}
\hypertarget{_ccp}{%
\subsubsection{CCP}\label{_ccp}}
(= l'équivalent du SRP, mais pour les composants)
\begin{quote}
If two classes are so tightly bound, either physically or conceptually,
that they always change together, then they belong in the same component
\end{quote}
Il y a peut-être aussi un lien à faire avec «~Your code as a crime
scene~» 🤟
\begin{verbatim}
La définition exacte devient celle-ci: « gather together those things that change at the same times and for the same reasons. Separate those things that change at different times or for different reasons ».
\end{verbatim}
\begin{verbatim}
==== CRP
\end{verbatim}
\begin{verbatim}
… que lon résumera ainsi: « dont depend on things you dont need » 😘
Au niveau des composants, au niveau architectural, mais également à dautres niveaux.
\end{verbatim}
\hypertarget{_sdp}{%
\subsubsection{SDP}\label{_sdp}}
(Stable dependency principle) qui définit une formule de stabilité pour
les composants, en fonction de sa faculté à être modifié et des
composants qui dépendent de lui: au plus un composant est nécessaire, au
plus il sera stable (dans la mesure où il lui sera difficile de
changer). En C++, cela correspond aux mots clés \#include. Pour
faciliter cette stabilité, il convient de passer par des interfaces
(donc, rarement modifiées, par définition).
En Python, ce ratio pourrait être calculé au travers des import, via les
AST.
\hypertarget{_sap}{%
\subsubsection{SAP}\label{_sap}}
(= Stable abstraction principle) pour la définition des politiques de
haut niveau vs les composants plus concrets. SAP est juste une
modélisation du OCP pour les composants: nous plaçons ceux qui ne
changent pas ou pratiquement pas le plus haut possible dans
l'organigramme (ou le diagramme), et ceux qui changent souvent plus bas,
dans le sens de stabilité du flux. Les composants les plus bas sont
considérés comme volatiles
\hypertarget{_pep257_docstring_conventions}{%
\subsection{PEP257 - Docstring
Conventions}\label{_pep257_docstring_conventions}}
\hypertarget{_formatage_de_code}{%
\subsection{Formatage de code}\label{_formatage_de_code}}
\hypertarget{_complexituxe9_cyclomatique}{%
\subsection{Complexité cyclomatique}\label{_complexituxe9_cyclomatique}}
A nouveau, un greffon pour \texttt{flake8} existe et donnera une
estimation de la complexité de McCabe pour les fonctions trop complexes.
Installez-le avec \texttt{pip\ install\ mccabe}, et activez-le avec le
paramètre \texttt{-\/-max-complexity}. Toute fonction dans la complexité
est supérieure à cette valeur sera considérée comme trop complexe.
\hypertarget{_typage_statique_pep585}{%
\subsection{\texorpdfstring{Typage statique -
\href{https://www.python.org/dev/peps/pep-0585/}{PEP585}}{Typage statique - PEP585}}\label{_typage_statique_pep585}}
Nous vous disions ci-dessus que Python était un langage dynamique
interprété. Concrètement, cela signifie que des erreurs pouvant être
détectées à la compilation avec d'autres langages, ne le sont pas avec
Python.
Il existe cependant une solution à ce problème, sous la forme de
\href{http://mypy-lang.org/}{Mypy}, qui peut (sous vous le souhaitez
;-)) vérifier une forme de typage statique de votre code source, grâce à
une expressivité du code, basée sur des annotations (facultatives, elles
aussi).
Ces vérifications se présentent de la manière suivante:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ typing }\ImportTok{import}\NormalTok{ List}
\KeywordTok{def}\NormalTok{ first\_int\_elem(l: List[}\BuiltInTok{int}\NormalTok{]) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{int}\NormalTok{:}
\ControlFlowTok{return}\NormalTok{ l[}\DecValTok{0}\NormalTok{] }\ControlFlowTok{if}\NormalTok{ l }\ControlFlowTok{else} \VariableTok{None}
\ControlFlowTok{if} \VariableTok{\_\_name\_\_} \OperatorTok{==} \StringTok{"\_\_main\_\_"}\NormalTok{:}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\DecValTok{1}\NormalTok{, }\DecValTok{2}\NormalTok{, }\DecValTok{3}\NormalTok{]))}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\StringTok{\textquotesingle{}a\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}b\textquotesingle{}}\NormalTok{, }\StringTok{\textquotesingle{}c\textquotesingle{}}\NormalTok{]))}
\end{Highlighting}
\end{Shaded}
Est-ce que le code ci-dessous fonctionne correctement ? \textbf{Oui}:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{python}\NormalTok{ mypy{-}test.py}
\ExtensionTok{1}
\ExtensionTok{a}
\end{Highlighting}
\end{Shaded}
Malgré que nos annotations déclarent une liste d'entiers, rien ne nous
empêche de lui envoyer une liste de caractères, sans que cela ne lui
pose de problèmes.
Est-ce que Mypy va râler ? \textbf{Oui, aussi}. Non seulement nous
retournons la valeur \texttt{None} si la liste est vide alors que nous
lui annoncions un entier en sortie, mais en plus, nous l'appelons avec
une liste de caractères, alors que nous nous attendions à une liste
d'entiers:
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{mypy}\NormalTok{ mypy{-}test.py}
\ExtensionTok{mypy{-}test.py}\NormalTok{:7: error: Incompatible return value type (got }\StringTok{"Optional[int]"}\NormalTok{, expected }\StringTok{"int"}\NormalTok{)}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 0 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 1 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{mypy{-}test.py}\NormalTok{:12: error: List item 2 has incompatible type }\StringTok{"str"}\KeywordTok{;} \ExtensionTok{expected} \StringTok{"int"}
\ExtensionTok{Found}\NormalTok{ 4 errors in 1 file (checked 1 source file)}
\end{Highlighting}
\end{Shaded}
Pour corriger ceci, nous devons:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Importer le type \texttt{Optional} et l'utiliser en sortie de notre
fonction \texttt{first\_int\_elem}
\item
Eviter de lui donner de mauvais paramètres ;-)
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ typing }\ImportTok{import}\NormalTok{ List, Optional}
\KeywordTok{def}\NormalTok{ first\_int\_elem(l: List[}\BuiltInTok{int}\NormalTok{]) }\OperatorTok{{-}\textgreater{}}\NormalTok{ Optional[}\BuiltInTok{int}\NormalTok{]:}
\ControlFlowTok{return}\NormalTok{ l[}\DecValTok{0}\NormalTok{] }\ControlFlowTok{if}\NormalTok{ l }\ControlFlowTok{else} \VariableTok{None}
\ControlFlowTok{if} \VariableTok{\_\_name\_\_} \OperatorTok{==} \StringTok{"\_\_main\_\_"}\NormalTok{:}
\BuiltInTok{print}\NormalTok{(first\_int\_elem([}\DecValTok{1}\NormalTok{, }\DecValTok{2}\NormalTok{, }\DecValTok{3}\NormalTok{]))}
\end{Highlighting}
\end{Shaded}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{λ }\ExtensionTok{mypy}\NormalTok{ mypy{-}test.py}
\ExtensionTok{Success}\NormalTok{: no issues found in 1 source file}
\end{Highlighting}
\end{Shaded}
\hypertarget{_tests_unitaires}{%
\subsubsection{Tests unitaires}\label{_tests_unitaires}}
\textbf{→ PyTest}
Comme tout bon \textbf{framework} qui se respecte, Django embarque tout
un environnement facilitant le lancement de tests; chaque application
est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la
classe \texttt{TestCase} depuis le package \texttt{django.test}:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{from}\NormalTok{ django.test }\ImportTok{import}\NormalTok{ TestCase}
\KeywordTok{class}\NormalTok{ TestModel(TestCase):}
\KeywordTok{def}\NormalTok{ test\_str(}\VariableTok{self}\NormalTok{):}
\ControlFlowTok{raise} \PreprocessorTok{NotImplementedError}\NormalTok{(}\StringTok{\textquotesingle{}Not implemented yet\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
Idéalement, chaque fonction ou méthode doit être testée afin de bien en
valider le fonctionnement, indépendamment du reste des composants. Cela
permet d'isoler chaque bloc de manière unitaire, et permet de ne pas
rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité
ou de la modification d'une existante. Il existe plusieurs types de
tests (intégration, comportement, \ldots\hspace{0pt}); on ne parlera ici
que des tests unitaires.
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux.
C'est là qu'il est utile d'avoir le pourcentage de code couvert par les
différents tests, pour savoir ce qui peut être amélioré.
Comme indiqué ci-dessus, Django propose son propre cadre de tests, au
travers du package \texttt{django.tests}. Une bonne pratique (parfois
discutée) consiste cependant à switcher vers \texttt{pytest}, qui
présente quelques avantages:
\begin{itemize}
\item
Une syntaxe plus concise (au prix de
\href{https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html}{quelques
conventions}, même si elles restent configurables): un test est une
fonction, et ne doit pas obligatoirement faire partie d'une classe
héritant de \texttt{TestCase} - la seule nécessité étant que cette
fonction fasse partie d'un module commençant ou finissant par "test"
(\texttt{test\_example.py} ou \texttt{example\_test.py}).
\item
Une compatibilité avec du code Python "classique" - vous ne devrez
donc retenir qu'un seul ensemble de commandes ;-)
\item
Des \emph{fixtures} faciles à réutiliser entre vos différents
composants
\item
Une compatibilité avec le reste de l'écosystème, dont la couverture de
code présentée ci-dessous.
\end{itemize}
Ainsi, après installation, il nous suffit de créer notre module
\texttt{test\_models.py}, dans lequel nous allons simplement tester
l'addition d'un nombre et d'une chaîne de caractères (oui, c'est
complètement biesse; on est sur la partie théorique ici):
\begin{Shaded}
\begin{Highlighting}[]
@ -293,8 +7,6 @@ complètement biesse; on est sur la partie théorique ici):
\end{Highlighting}
\end{Shaded}
Forcément, cela va planter. Pour nous en assurer (dès fois que quelqu'un
en doute), il nous suffit de démarrer la commande \texttt{pytest}:
\begin{Shaded}
\begin{Highlighting}[]
@ -325,24 +37,6 @@ en doute), il nous suffit de démarrer la commande \texttt{pytest}:
\hypertarget{_couverture_de_code}{%
\subsubsection{Couverture de code}\label{_couverture_de_code}}
La couverture de code est une analyse qui donne un pourcentage lié à la
quantité de code couvert par les tests. Attention qu'il ne s'agit pas de
vérifier que le code est \textbf{bien} testé, mais juste de vérifier
\textbf{quelle partie} du code est testée. Le paquet \texttt{coverage}
se charge d'évaluer le pourcentage de code couvert par les tests.
Avec \texttt{pytest}, il convient d'utiliser le paquet
\href{https://pypi.org/project/pytest-cov/}{\texttt{pytest-cov}}, suivi
de la commande \texttt{pytest\ -\/-cov=gwift\ tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez
passer par le paquet
\href{https://pypi.org/project/django-coverage-plugin/}{django-coverage-plugin}
Ajoutez-le dans le fichier \texttt{requirements/base.txt}, et lancez une
couverture de code grâce à la commande \texttt{coverage}. La
configuration peut se faire dans un fichier \texttt{.coveragerc} que
vous placerez à la racine de votre projet, et qui sera lu lors de
l'exécution.
\begin{Shaded}
\begin{Highlighting}[]
@ -399,55 +93,7 @@ l'exécution.
Ceci vous affichera non seulement la couverture de code estimée, et
générera également vos fichiers sources avec les branches non couvertes.
\hypertarget{_matrice_de_compatibilituxe9}{%
\subsubsection{Matrice de
compatibilité}\label{_matrice_de_compatibilituxe9}}
L'intérêt de la matrice de compatibilité consiste à spécifier un
ensemble de plusieurs versions d'un même interpréteur (ici, Python),
afin de s'assurer que votre application continue à fonctionner. Nous
sommes donc un cran plus haut que la spécification des versions des
librairies, puisque nous nous situons directement au niveau de
l'interpréteur.
L'outil le plus connu est
\href{https://tox.readthedocs.io/en/latest/}{Tox}, qui consiste en un
outil basé sur virtualenv et qui permet:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
de vérifier que votre application s'installe correctement avec
différentes versions de Python et d'interpréteurs
\item
de démarrer des tests parmi ces différents environnements
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# content of: tox.ini , put in same dir as setup.py}
\KeywordTok{[tox]}
\DataTypeTok{envlist }\OtherTok{=}\StringTok{ py36,py37,py38,py39}
\DataTypeTok{skipsdist }\OtherTok{=}\StringTok{ }\KeywordTok{true}
\KeywordTok{[testenv]}
\DataTypeTok{deps }\OtherTok{=}
\DataTypeTok{ {-}r requirements/dev.txt}
\DataTypeTok{commands }\OtherTok{=}
\DataTypeTok{ pytest}
\end{Highlighting}
\end{Shaded}
Démarrez ensuite la commande \texttt{tox}, pour démarrer la commande
\texttt{pytest} sur les environnements Python 3.6, 3.7, 3.8 et 3.9,
après avoir installé nos dépendances présentes dans le fichier
\texttt{requirements/dev.txt}.
pour que la commande ci-dessus fonctionne correctement, il sera
nécessaire que vous ayez les différentes versions d'interpréteurs
installées. Ci-dessus, la commande retournera une erreur pour chaque
version non trouvée, avec une erreur type
\texttt{ERROR:\ \ \ pyXX:\ InterpreterNotFound:\ pythonX.X}.
\hypertarget{_configuration_globale}{%
\subsubsection{Configuration globale}\label{_configuration_globale}}

View File

@ -706,268 +706,276 @@ Ceci autorise une forme d'immunité entre les composants.
\section{Composants}
\subsection{Reuse/Release Equivalence}
\begin{quote}
Classes and modules that are grouped together into a component should be releasable together \cite[p. 105]{clean_architecture}
\end{quote}
\subsection{CCP}
\begin{quote}
If two classes are so tightly bound, either physically or conceptually, that they always change together, then they belong in the same component.
\end{quote}
Plus spécifiquement, la définition exacte devient celle-ci:
\begin{quote}
Gather together those things that change at the same times and for the same reasons.
Separate those things that change at different times or for different reasons.
\end{quote}
Que l'on résumera ainsi: "dont depend on things you dont need", comme nous l'avons déjà expliqué plus haut.
\subsection{Stable dependency principle}
Ce principe définit une formule de stabilité pour les composants, en fonction de leur faculté à être modifié et des composants qui dépendent de lui: au plus un composant est nécessaire, au plus il sera stable (dans la mesure où il lui sera difficile de changer).
En C++, cela correspond aux mots clés \#include.
Pour faciliter cette stabilité, il convient de passer par des interfaces (donc, rarement modifiées, par définition).
En Python, ce ratio pourrait être calculé au travers des import, via les AST.
\subsection{Stable Abstraction Principle}
Ce principe-ci définit les politiques de haut niveau vs les composants plus concrets.
SAP est juste une modélisation du OCP pour les composants: nous plaçons ceux qui ne changent pas ou pratiquement pas le plus haut possible dans l'organigramme (ou le diagramme), et ceux qui changent souvent plus bas, dans le sens de stabilité du flux.
Les composants les plus bas sont considérés comme volatiles.
\section{Architecture générale}
\begin{quote}
If you think good architecture is expensive, try bad architecture
--- Brian Foote and Joseph Yoder
\end{quote}
--- Brian Foote \& Joseph Yoder
\end{quote}
La flexiiblité de l'architecture générale est moins permissive que celle allouée aux modules ou aux composants, dans la mesure où elle définit en partie les frontières et interactions possibles avec le monde extérieur.
Il est nécessaire de projeter la capacité d'adaptation, en minimisant la maintenance.
Un des problèmes est qu'à la première demande, l'architecture pourrait avoir pris une mauvaise direction, sans aucune malléabilité.
Une bonne architecture va rendre le système facile à lire, facile à développer, facile à maintenir et facile à déployer, l'objectif ultime étant de minimiser le coût de maintenance et de maximiser la productivité des développeurs.
Un des autres objectifs d'une bonne architecture consiste à se garder le plus d'options possibles, et à se concentrer sur les détails (le type de base de données, la conception concrète, ...) le plus tard possible, tout en conservant la politique principale en ligne de mire.
Ceci permet de délayer les choix techniques à «~plus tard~», ce qui permet également de concrétiser ces choix en ayant le plus d'informations possibles \cite[pp.137-141]{clean_architecture}
Derrière une bonne architecture, il y a aussi un investissement quant aux ressources qui seront nécessaires à faire évoluer l'application: ne
pas investir dès qu'on le peut va juste lentement remplir la case de la dette technique.
Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le développement est suivi et que les gestionnaires (et architectes) s'engagent à économiser du temps et de la qualité lorsque des changements seront demandés pour l'évolution du projet: ne pas investir à améliorer l'architecture dès que ce sera possible fera lentement (mais sûrement!) dériver la base de code vers une augmentation de la dette technique.
Faire évoluer correctement l'architecture d'un projet demande une bonne expérience, mais également un bon sens de l'observation, un investissement non négligeable en attention portée aux détails et de la patience:
\begin{quote}
This is not a one time decision.
You don't simply decide at the start of a project which boundaries to implement and which to ignore.
Rather, you watch.
You pay attention as the system evolves.
Au delà des principes dont il est question plus haut, c'est dans les
ressources proposées et les cas démontrés que l'on comprend leur
intérêt: plus que de la définition d'une architecture adéquate, c'est
surtout dans la facilité de maintenance d'une application que ces
principes s'identifient.
Une bonne architecture va rendre le système facile à lire, facile à
développer, facile à maintenir et facile à déployer. L'objectif ultime
étant de minimiser le coût de maintenance et de maximiser la
productivité des développeurs. Un des autres objectifs d'une bonne
architecture consiste également à se garder le plus d'options possibles,
et à se concentrer sur les détails (le type de base de données, la
conception concrète, le plus tard possible, tout en
conservant la politique principale en ligne de mire. Cela permet de
délayer les choix techniques à «~plus tard~», ce qui permet également de
concrétiser ces choix en ayant le plus d'informations possibles
\cite[pp.137-141]{clean_architecture}
Derrière une bonne architecture, il y a aussi un investissement quant
aux ressources qui seront nécessaires à faire évoluer l'application: ne
pas investir dès qu'on le peut va juste lentement remplir la case de la
dette technique.
Une architecture ouverte et pouvant être étendue n'a d'intérêt que si le
développement est suivi et que les gestionnaires (et architectes)
s'engagent à économiser du temps et de la qualité lorsque des
changements seront demandés pour l'évolution du projet.
\section{Politiques et règles métiers}
\section{Considérations sur les frameworks}
\begin{quote}
Frameworks are tools to be used, not architectures to be conformed to.
Your architecture should tell readers about the system, not about the
frameworks you used in your system. If you are building a health care
system, then when new programmers look at the source repository, their
first impression should be, «~oh, this is a health care system~». Those
new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered.
--- Robert C. Martin Clean Architecture
\end{quote}
Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et
pas une obligation de structuration. L'idée est que le framework doit se
conformer à la définition de l'application, et non l'inverse. Dans le
cadre de l'utilisation de Django, c'est un point critique à prendre en
considération: une fois que vous aurez fait ce choix, vous aurez
extrêmement difficile à faire machine arrière:
\begin{itemize}
\item
Votre modèle métier sera largement couplé avec le type de base de
données (relationnelle, indépendamment
\item
Votre couche de présentation sera surtout disponible au travers d'un
navigateur
\item
Les droits d'accès et permissions seront en grosse partie gérés par le
frameworks
\item
La sécurité dépendra de votre habilité à suivre les versions
\item
Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu
le temps de développer) dépendront de la bonne volonté de la
communauté
\end{itemize}
Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une
fois que vous aurez défini la politique, les règles métiers, les données
critiques et entités, et que vous aurez fait le choix de développer en
âme et conscience votre nouvelle création en utilisant Django, vous
serez bon gré mal gré, contraint de continuer avec. Cette décision ne
sera pas irrévocable, mais difficile à contourner.
\begin{quote}
At some point in their history most DevOps organizations were hobbled by
tightly-coupled, monolithic architectures that while extremely
successfull at helping them achieve product/market fit - put them at
risk of organizational failure once they had to operate at scale (e.g.
eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS
application in 2001, Twitter's monolithic Rails front-end in 2009, and
LinkedIn's monolithic Leo application in 2011). In each of these cases,
they were able to re-architect their systems and set the stage not only
to survice, but also to thrise and win in the marketplace.
\cite[182]{devops_handbook}
\end{quote}
Ceci dit, Django compense ses contraintes en proposant énormément de
flexibilité et de fonctionnalités \textbf{out-of-the-box}, c'est-à-dire
que vous pourrez sans doute avancer vite et bien jusqu'à un point de
rupture, puis revoir la conception et réinvestir à ce moment-là, mais en
toute connaissance de cause.
\begin{quote}
When any of the external parts of the system become obsolete, such as
the database, or the web framework, you can replace those obsolete
elements with a minimum of fuss.
--- Robert C. Martin Clean Architecture
\end{quote}
Avec Django, la difficulté à se passer du framework va consister à
basculer vers «~autre chose~» et a remplacer chacune des tentacules qui
aura pousser partout dans l'application.
A noter que les services et les «~architectures orientées services~» ne
sont jamais qu'une définition d'implémentation des frontières, dans la
mesure où un service n'est jamais qu'une fonction appelée au travers
d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application
monolotihique sera tout aussi fonctionnelle qu'une application découpée
en microservices. \cite[p. 243]{clean_architecture}
\section{Inversion de dépendances}
Dans la partie SOLID, nous avons évoqué plusieurs principes de
développement. Django est un framework qui évolue, et qui a pu présenter
certains problèmes liés à l'un de ces principes.
Les link:release
notes{[}\url{https://docs.djangoproject.com/en/2.0/releases/2.0/}{]} de
Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite
l'abandon du support d'link:Oracle
11.2{[}\url{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{]}.
En substance, cela signifie que le framework se chargeait lui-même de
construire certaines parties de requêtes, qui deviennent non
fonctionnelles dès lors que l'on met le framework ou le moteur de base
de données à jour. Réécrit, cela signifie que:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous
serez limité à une version 1.11 de Django
\item
Tandis que si votre moteur est géré par une version ultérieure, le
framework pourra être mis à jour.
\end{enumerate}
Nous sommes dans un cas concret d'inversion de dépendances ratée: le
framework (et encore moins vos politiques et règles métiers) ne
devraient pas avoir connaissance du moteur de base de données. Pire, vos
politiques et données métiers ne devraient pas avoir connaissance
\textbf{de la version} du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (\textbf{la
base de données}) a une incidence directe sur les fonctionnalités mises
à disposition par votre application, ce qui va à l'encontre des 12
facteurs (et des principes de développement).
Ce point sera rediscuté par la suite, notamment au niveau de l'épinglage
des versions, de la reproduction des environnements et de
l'interdépendance entre des choix techniques et fonctionnels.
\section{Conclusions}
\begin{quote}
La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.
-- Antoine de Saint-Exupéry
\end{quote}
Il est impossible de se projeter dans le futur d'une application: il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web.
Ce sont des choses qui viennent avec l'expérience (ou avec la tordure d'esprit \footnote{Si, ça existe}).
Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt: il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework}
Il est impossible d'imaginer ou de se projeter dans tous les éléments qui pourraient devoir être modifiés après que votre développement ait été livrée.
En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où une décision pourrait devoir être faite.
A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe.
Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire), ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis.
Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes.
Avec cette approche, les composants seront déjà découplés au mieux.
En terme de découpe, les composants peuvent l'être aux niveaux suivants:
\begin{itemize}
\item
\textbf{Code source}
\item
\textbf{Déploiement}, au travers de dll, jar, linked libraries, \ldots{} voire
au travers de threads ou de processus locaux.
\item
\textbf{Mise à disposition de services}
\end{itemize}
Cette section se base sur deux ressources principales \cite{maintainable_software} \cite{clean_code}, qui répartissent un ensemble de conseils parmi quatre niveaux de composants:
\begin{itemize}
\item
Les méthodes et fonctions
\item
Les classes
\item
Les composants
\item
Et des conseils plus généraux.
\end{itemize}
\subsection{Au niveau des méthodes et fonctions}
\begin{itemize}
\item
\textbf{Gardez vos méthodes/fonctions courtes}. Pas plus de 15 lignes, en comptant les commentaires.
Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9\% de plus de 60 lignes; pas plus de 22.3\% de plus de 30 lignes, au plus 43.7\% de plus de 15 lignes et au moins 56.3\% en dessous de 15 lignes).
Oui, c'est dur à tenir, mais faisable.
\item
\textbf{Conserver une complexité de McCabe en dessous de 5}, c'est-à-dire avec quatre branches au maximum.
A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
\item
\textbf{N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.}: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées.
\item
\textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}. Quatre paramètres, pas plus.
Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester.
\end{itemize}
\subsection{Au niveau des classes}
\begin{itemize}
\item
\textbf{Privilégiez un couplage faible entre vos classes}.
Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif.
L'implémentation du service \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{UserService}.
De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction.
La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
\end{itemize}
\subsection{Au niveau des composants}
\begin{itemize}
\item
\textbf{Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants} également.
Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture.
Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "\emph{juste parce qu'elle en a la possibilité}".
Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}).
\item
\textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{A} ne soit un énorme mastodonte, alors que le
composant juste à côté ne soit capable que d'une action.
De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à
gérer.
Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.
\end{itemize}
\subsection{De manière générale}
\begin{itemize}
\item
\textbf{Conserver une densité de code faible}: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en
moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois.
Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisantdu refactoring (intensif), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux.
Mais surtout, en évitant de copier/coller bêtement du code existant.
\item
\textbf{Automatiser les tests}, en ajoutant un environnement d'intégration continue dès le début du projet et en faisant vérifier par des outils automatiques tous les points ci-dessus.
\end{itemize}
You note where boundaries may be required, and then carefully watch for the first inkling of friction because those boundaries don't exist.
At that point, you weight the costs of implementing those boundaries versus the cost of ignoring them and you review that decision frequently. Your goal is to implement the boundaries right at the inflection point where the cost of implementing becomes less than the cost of ignoring.
\end{quote}
\section{Politiques et règles métiers}
\section{Considérations sur les frameworks}
\begin{quote}
Frameworks are tools to be used, not architectures to be conformed to.
Your architecture should tell readers about the system, not about the
frameworks you used in your system. If you are building a health care
system, then when new programmers look at the source repository, their
first impression should be, «~oh, this is a health care system~». Those
new programmers should be able to learn all the use cases of the system,
yet still not know how the system is delivered.
--- Robert C. Martin Clean Architecture
\end{quote}
Le point soulevé ci-dessous est qu'un framework n'est qu'un outil, et
pas une obligation de structuration. L'idée est que le framework doit se
conformer à la définition de l'application, et non l'inverse. Dans le
cadre de l'utilisation de Django, c'est un point critique à prendre en
considération: une fois que vous aurez fait ce choix, vous aurez
extrêmement difficile à faire machine arrière:
\begin{itemize}
\item
Votre modèle métier sera largement couplé avec le type de base de
données (relationnelle, indépendamment
\item
Votre couche de présentation sera surtout disponible au travers d'un
navigateur
\item
Les droits d'accès et permissions seront en grosse partie gérés par le
frameworks
\item
La sécurité dépendra de votre habilité à suivre les versions
\item
Et les fonctionnalités complémentaires (que vous n'aurez pas voulu/eu
le temps de développer) dépendront de la bonne volonté de la
communauté
\end{itemize}
Le point à comprendre ici n'est pas que "Django, c'est mal", mais qu'une
fois que vous aurez défini la politique, les règles métiers, les données
critiques et entités, et que vous aurez fait le choix de développer en
âme et conscience votre nouvelle création en utilisant Django, vous
serez bon gré mal gré, contraint de continuer avec. Cette décision ne
sera pas irrévocable, mais difficile à contourner.
\begin{quote}
At some point in their history most DevOps organizations were hobbled by
tightly-coupled, monolithic architectures that while extremely
successfull at helping them achieve product/market fit - put them at
risk of organizational failure once they had to operate at scale (e.g.
eBay's monolithic C++ application in 2001, Amazon's monolithic OBIDOS
application in 2001, Twitter's monolithic Rails front-end in 2009, and
LinkedIn's monolithic Leo application in 2011). In each of these cases,
they were able to re-architect their systems and set the stage not only
to survice, but also to thrise and win in the marketplace.
\cite[182]{devops_handbook}
\end{quote}
Ceci dit, Django compense ses contraintes en proposant énormément de
flexibilité et de fonctionnalités \textbf{out-of-the-box}, c'est-à-dire
que vous pourrez sans doute avancer vite et bien jusqu'à un point de
rupture, puis revoir la conception et réinvestir à ce moment-là, mais en
toute connaissance de cause.
\begin{quote}
When any of the external parts of the system become obsolete, such as
the database, or the web framework, you can replace those obsolete
elements with a minimum of fuss.
--- Robert C. Martin Clean Architecture
\end{quote}
Avec Django, la difficulté à se passer du framework va consister à
basculer vers «~autre chose~» et a remplacer chacune des tentacules qui
aura pousser partout dans l'application.
A noter que les services et les «~architectures orientées services~» ne
sont jamais qu'une définition d'implémentation des frontières, dans la
mesure où un service n'est jamais qu'une fonction appelée au travers
d'un protocole (rest, soap, \ldots\hspace{0pt}). Une application
monolotihique sera tout aussi fonctionnelle qu'une application découpée
en microservices. \cite[p. 243]{clean_architecture}
\section{Inversion de dépendances}
Dans la partie SOLID, nous avons évoqué plusieurs principes de
développement. Django est un framework qui évolue, et qui a pu présenter
certains problèmes liés à l'un de ces principes.
Les \href{https://docs.djangoproject.com/en/2.0/releases/2.0/}{Releases Notes} de Django 2.0 date de décembre 2017; parmi ces notes, l'une d'elles cite l'abandon du support d'\href{https://docs.djangoproject.com/en/2.0/releases/2.0/\#dropped-support-for-oracle-11-2}{Oracle 11.2}.
En substance, cela signifie que le framework se chargeait lui-même de construire certaines parties de requêtes, qui deviennent non fonctionnelles dès lors que l'on met le framework ou le moteur de base de données à jour.
Réécrit, cela signifie que:
\begin{enumerate}
\item Si vos données sont stockées dans un moteur géré par Oracle 11.2, vous serez limité à une version 1.11 de Django
\item Tandis que si votre moteur est géré par une version ultérieure, le framework pourra être mis à jour.
\end{enumerate}
Nous sommes dans un cas concret d'inversion de dépendances ratée: le framework (et encore moins vos politiques et règles métiers) ne devraient pas avoir connaissance du moteur de base de données.
Pire, vos politiques et données métiers ne devraient pas avoir connaissance \textbf{de la version} du moteur de base de données.
En conclusion, le choix d'une version d'un moteur technique (\textbf{la base de données}) a une incidence directe sur les fonctionnalités mises à disposition par votre application, ce qui va à l'encontre des 12 facteurs (et des principes de développement).
Ce point sera rediscuté par la suite, notamment au niveau de l'épinglage des versions, de la reproduction des environnements et de l'interdépendance entre des choix techniques et fonctionnels.
\section{Conclusions}
\begin{quote}
La perfection est atteinte, non pas lorsqu'il n'y a plus rien à ajouter, mais lorsqu'il n'y a plus rien à retirer.
-- Antoine de Saint-Exupéry
\end{quote}
Il est impossible de se projeter dans le futur d'une application: il est impossible d'imaginer les modifications qui seront demandées par les utilisateurs, de se projeter dans l'évolution d'un langage, dans les nécessités d'intégration de certaines librairies ou dans le support-même de certaines fonctionnalités par les navigateurs Web.
Ce sont des choses qui viennent avec l'expérience (ou avec la tordure d'esprit \footnote{Si, ça existe}).
Cela rejoint le fameux "YAGNI\index{YAGNI}" dont nous parlions plus tôt: il est inutile de vouloir développer absolument toutes les fonctionnalités qui pourraient un jour pouvoir être utilisées ou souhaitées, car cela complexifiera autant le code, que les déploiement, l'utilisabilité ou la compréhension que les utilisateurs pourront avoir de votre application. \cite{rework}
Il est impossible d'imaginer ou de se projeter dans tous les éléments qui pourraient devoir être modifiés après que votre développement ait été livrée.
En ayant connaissance de toutes les choses qui pourraient être modifiées par la suite, lidée est de pousser le développement jusquau point où une décision pourrait devoir être faite.
A ce stade, larchitecture nécessitera des modifications, mais aura déjà intégré le fait que cette possibilité existe.
Nous nallons donc pas jusquau point où le service doit être créé (même sil peut ne pas être nécessaire), ni à lextrême au fait dignorer quun service pourrait être nécessaire, mais nous aboutissons à une forme de compromis.
Une forme d'application de la philosophie de René Descartes, où le fait de seulement envisager une possibilité ouvre un maximum de portes.
Avec cette approche, les composants seront déjà découplés au mieux.
Les composants peuvent être découpés au niveau:
\begin{itemize}
\item \textbf{Du code source}, via des modules, paquets, dépendances, ...
\item \textbf{Du déploiement ou de l'exécution}, au travers de dll, jar, linked libraries, ..., voire au travers de threads ou de processus locaux.
\item \textbf{Via la mise à disposition de nouveaux services}, lorsqu'il est plus intuitif de contacter un nouveau point de terminaison que d'intégrer de force de nouveaux concepts dans une base de code existante.
\end{itemize}
Cette section se base sur deux ressources principales \cite{maintainable_software} \cite{clean_code}, qui répartissent un ensemble de conseils parmi quatre niveaux de composants:
\begin{itemize}
\item Les méthodes et fonctions
\item Les classes
\item Les composants
\item Et des conseils plus généraux.
\end{itemize}
\subsection{Au niveau des méthodes et fonctions}
\begin{itemize}
\item
\textbf{Gardez vos méthodes/fonctions courtes}. Pas plus de 15 lignes, en comptant les commentaires.
Des exceptions sont possibles, mais dans une certaine mesure uniquement (pas plus de 6.9\% de plus de 60 lignes; pas plus de 22.3\% de plus de 30 lignes, au plus 43.7\% de plus de 15 lignes et au moins 56.3\% en dessous de 15 lignes).
Oui, c'est dur à tenir, mais faisable.
\item
\textbf{Conserver une complexité de McCabe en dessous de 5}, c'est-à-dire avec quatre branches au maximum.
A nouveau, si une méthode présente une complexité cyclomatique de 15, la séparer en 3 fonctions ayant chacune une complexité de 5 conservera la complexité globale à 15, mais rendra le code de chacune de ces méthodes plus lisible, plus maintenable.
\item
\textbf{N'écrivez votre code qu'une seule fois: évitez les duplications, copie, etc.}: imaginez qu'un bug soit découvert dans une fonction; il devra alors être corrigé dans toutes les fonctions qui auront été copiées/collées.
\item
\textbf{Conservez de petites interfaces et signatures de fonctions/méthodes}. Quatre paramètres, pas plus.
Au besoin, refactorisez certains paramètres dans une classe ou une structure, qui sera plus facile à tester.
\end{itemize}
\subsection{Au niveau des classes}
\begin{itemize}
\item
\textbf{Privilégiez un couplage faible entre vos classes}.
Ceci n'est pas toujours possible, mais dans la mesure du possible, éclatez vos classes en fonction de leur domaine de compétences respectif.
L'implémentation du service \texttt{UserNotificationsService} ne doit pas forcément se trouver embarqué dans une classe \texttt{UserService}.
De même, pensez à passer par une interface (commune à plusieurs classes), afin d'ajouter une couche d'abstraction.
La classe appellante n'aura alors que les méthodes offertes par l'interface comme points d'entrée.
\end{itemize}
Dans la même veine, faites en sorte que les dépendances aillent toutes "dans le même sens", ce qui limitera l'effet spaghetti associé au code, tout en améliorant sa lisibilité et l'intuitivité de sa compréhension.
\subsection{Au niveau des composants}
\begin{itemize}
\item
\textbf{Tout comme pour les classes, il faut conserver un couplage faible au niveau des composants} également.
Une manière d'arriver à ce résultat est de conserver un nombre de points d'entrée restreint, et d'éviter qu'il ne soit possible de contacter trop facilement des couches séparées de l'architecture.
Pour une architecture n-tiers par exemple, la couche d'abstraction à la base de données ne peut être connue que des services; sans cela, au bout de quelques semaines, n'importe quelle couche de présentation risque de contacter directement la base de données, "\emph{juste parce qu'elle en a la possibilité}".
Vous pourriez également passer par des interfaces, afin de réduire le nombre de points d'entrée connus par un composant externe (qui ne connaîtra par exemple que \texttt{IFileTransfer} avec ses méthodes \texttt{put} et \texttt{get}, et non pas les détailsd'implémentation complet d'une classe \texttt{FtpFileTransfer} ou \texttt{SshFileTransfer}).
\item
\textbf{Conserver un bon balancement au niveau des composants}: évitez qu'un composant \textbf{A} ne soit un énorme mastodonte, alors que le
composant juste à côté ne soit capable que d'une action.
De cette manière, les nouvelles fonctionnalités seront mieux réparties parmi les différents systèmes, et les responsabilités seront plus faciles à
gérer.
Un conseil est d'avoir un nombre de composants compris entre 6 et 12 (idéalement, 12), et que chacun de ces composants soit approximativement de même taille.
\end{itemize}
\subsection{De manière générale}
\begin{itemize}
\item
\textbf{Conserver une densité de code faible}: il n'est évidemment pas possible d'implémenter n'importe quelle nouvelle fonctionnalité en
moins de 20 lignes de code; l'idée ici est que la réécriture du projet ne prenne pas plus de 20 hommes/mois.
Pour cela, il faut (activement) passer du temps à réduire la taille du code existant: soit en faisantdu refactoring (intensif), soit en utilisant des librairies existantes, soit en explosant un système existant en plusieurs sous-systèmes communiquant entre eux.
Mais surtout, en évitant de copier/coller bêtement du code existant.
\item
\textbf{Automatiser les tests}, en ajoutant un environnement d'intégration continue dès le début du projet et en faisant vérifier par des outils automatiques tous les points ci-dessus.
\end{itemize}

28
chapters/new-project.tex Normal file
View File

@ -0,0 +1,28 @@
\chapter{Démarrer un nouveau projet}
chaque application est créée par défaut avec un fichier \textbf{tests.py}, qui inclut la classe \texttt{TestCase} depuis le package \texttt{django.test}:
Comme indiqué ci-dessus, Django propose son propre cadre de tests, au travers du package \texttt{django.tests}.
\section{Tests unitaires}
On a deux choix ici:
\begin{enumerate}
\item Utiliser les librairies de test de Django
\item Utiliser Pytest
\end{enumerate}
\subsection{django.test}
\begin{listing}[H]
\begin{minted}{Python}
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
\end{minted}
\end{listing}
\subsection{Pytest}

View File

@ -60,87 +60,67 @@ permet de surcharger l'initialisation d'une instance de classe.
\end{minted}
\end{listing}
Ces méthodes, utilisées seules ou selon des combinaisons spécifiques,
constituent les \emph{protocoles de langage}. Une liste complètement des
\emph{dunder methods} peut être trouvée dans la section
\texttt{Data\ Model} de
\href{https://docs.python.org/3/reference/datamodel.html}{la
documentation du langage Python}.
Ces méthodes, utilisées seules ou selon des combinaisons spécifiques, constituent les \emph{protocoles de langage}.
Une liste complètement des \emph{dunder methods} peut être trouvée dans la section \texttt{Data\ Model} de \href{https://docs.python.org/3/reference/datamodel.html}{la documentation du langage Python}.
All operators are also exposed as ordinary functions in the operators
module. The documentation of that module gives a good overview of Python
operators. It can be found at
\url{https://docs.python.org/3.9/library/operator.html}
Tous les opérateurs sont également exposés comme des fonctions ordinaires du module \texttt{opeartor}, dont la \href{https://docs.python.org/3.9/library/operator.html}{documentation} donne un bon aperçu.
Indiquer qu'un type d'objet implémente un protocole du langage indique simplement qu'il est compatible avec une partie spécifique de la syntaxe du langage Python.
If we say that an object implements a specific language protocol, it
means that it is compatible with a specific part of the Python language
syntax.
Vous trouverez ci-dessous un tableau reprenant les protocoles les plus courants:
The following is a table of the most common protocols within the Python
language.
\begin{center}
\begin{tabular}{ c c c }
cell1 & cell2 & cell3 \\
cell4 & cell5 & cell6 \\
cell7 & cell8 & cell9
\end{tabular}
\end{center}
Protocol nameMethodsDescriptionCallable protocol\emph{call}()Allows
objects to be called with parentheses:instance()Descriptor
protocols\emph{set}(), \emph{get}(), and \emph{del}()Allows us to
manipulate the attribute access pattern of classes (see the Descriptors
section)Container protocol\emph{contains}()Allows us to test whether or
not an object contains some value using the in keyword:value in instance
Les points principaux à présenter ci-dessus:
Python in Comparison with Other LanguagesIterable
protocol\emph{iter}()Allows objects to be iterated using the
forkeyword:for value in instance: \ldots\hspace{0pt}Sequence
protocol\emph{getitem}(),\emph{len}()Allows objects to be indexed with
square bracket syntax and queried for length using a built-in
function:item = instance{[}index{]}length = len(instance)Each operator
available in Python has its own protocol and operator overloading
happens by implementing the dunder methods of that protocol. Python
provides over 50 overloadable operators that can be divided into five
main groups:• Arithmetic operators • In-place assignment operators•
Comparison operators• Identity operators• Bitwise operatorsThat's a lot
of protocols so we won't discuss all of them here. We will instead take
a look at a practical example that will allow you to better understand
how to implement operator overloading on your own
\begin{enumerate}
\item \_\_lt\_\_, \_\_gt\_\_
\item item()
\item getitem() - pour utiliser les []
\item len() - pour connaître la longueur d'un objet
\item ...
\end{enumerate}
The \texttt{add()} method is responsible for overloading the \texttt{+}
(plus sign) operator and here it allows us to add two matrices together.
Only matrices of the same dimensions can be added together. This is a
fairly simple operation that involves adding all matrix elements one by
one to form a new matrix.
Le langage autorise nativement plus d'une cinquantaine d'opérateurs différents:
The \texttt{sub()} method is responsible for overloading the \texttt{}
(minus sign) operator that will be responsible for matrix subtraction.
To subtract two matrices, we use a similar technique as in the --
operator:
\begin{enumerate}
\item Opérateurs arithmétiques
\item Opérateurs d'assignation
\item Opérateurs de comparaisons
\item Opérateurs d'identité
\item Opérateurs de comparaison bit à bit
\item ...
\end{enumerate}
Par exemple, la méthode \texttt{add()} est responsable de l'implémentation de l'opérateur \texttt{+}, la méthode \texttt{sub()} s'occupe de la soustraction ()\texttt{}), tandis que \texttt{mul()} gère l'opérateur \texttt{*}.
L'exemple ci-dessous implémente la soustraction de deux matrices:
\begin{listing}
\begin{minted}{python}
\begin{minted}[tabsize=4]{python}
def __sub__(self, other):
if (len(self.rows) != len(other.rows) or len(self.rows[0]) != len(other.rows[0])):
if (len(self.rows) != len(other.rows)
or len(self.rows[0]) != len(other.rows[0])
):
raise ValueError("Matrix dimensions don't match")
return Matrix([[a - b for a, b in zip(a_row, b_row)] for a_row, b_row in zip(self.rows, other.rows) ])
return Matrix(
[
[a - b for a, b in zip(a_row, b_row)]
for a_row, b_row in zip(self.rows, other.rows)
]
)
\end{minted}
\end{listing}
The last overloaded operator is the most complex one. This is the
\texttt{*} operator, which is implemented through the \texttt{mul()}
method. In linear algebra, matrices don't have the same multiplication
operation as real numbers. Two matrices can be multiplied if the first
matrix has a number of columns equal to the number of rows of the second
matrix. The result of that operation is a new matrix where each element
is a dot product of the corresponding row of the first matrix and the
corresponding column of the second matrix. Here we've built our own
implementation of the matrix to present the idea of operators
overloading. Although Python lacks a built-in type for matrices, you
don't need to build them from scratch. The NumPy package is one of the
best Python mathematical packages and among others provides native
support for matrix algebra. You can easily obtain the NumPy package from
PyPI
Il suffit dès lors de réaliser la soustraction matricielle entre deux objets de type \texttt{Matrix} pour que la méthode \texttt{sub())} ci-dessous soit appelée.
En fait, l'intérêt concerne surtout la représentation de nos modèles,
puisque chaque classe du modèle est représentée par la définition d'un
objet Python. Nous pouvons donc utiliser ces mêmes \textbf{dunder
methods} (\textbf{double-underscores methods}) pour étoffer les
protocoles du langage.
En fait, l'intérêt concerne surtout la représentation de nos modèles, puisque chaque classe du modèle est représentée par la définition d'un objet Python.
Nous pouvons donc utiliser ces mêmes \textbf{dunder methods} (\textbf{double-underscores methods}) pour étoffer les protocoles du langage.
\section{The Zen of Python}
@ -179,13 +159,13 @@ La première PEP qui va nous intéresser est la PEP8.
Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, ...
En bref, elle décrit comment écrire du code proprement, afin que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Dans cet objectif, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip.
Lancez ensuite la commande pep8 suivie du chemin à analyser (\texttt{.}, le nom d'un répertoire, le nom d'un fichier \texttt{.py}, \ldots\hspace{0pt}).
Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options \texttt{-\/-statistics\ -qq}.
Dans cet objectif, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: flake8. Pour l'installer, passez par pip.
Lancez ensuite la commande \texttt{flake8} suivie du chemin à analyser (\texttt{.}, le nom d'un répertoire, le nom d'un fichier \texttt{.py}, ...).
Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options \texttt{-\/-statistics\ -qq} - l'attribut \texttt{-qq} permettant simplement d'ignorer toute sortie console autre que les statistiques demandées).
\begin{listing}[!ht]
\begin{verbatim}
$ pep8 . --statistics -qq
$ flake8 . --statistics -qq
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
@ -195,17 +175,28 @@ $ pep8 . --statistics -qq
13 E202 whitespace before '}'
86 E203 whitespace before ':'
\end{verbatim}
\caption{Une utilisation de pep8}
\caption{Une utilisation de flake8}
\end{listing}
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez \texttt{pyflakes}: cette librairie analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
\begin{listing}[!ht]
\begin{verbatim}
$ pyflakes .
...
\end{verbatim}
\caption{Une utilisation de pyflakes}
\end{listing}
A noter qu'un greffon pour \texttt{flake8} existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes.
Installez-le avec \texttt{pip\ install\ mccabe}, et activez-le avec le paramètre \texttt{-\/-max-complexity}.
Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
\section{Conventions de documentation}
Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code.
Cela impose une certaine rigueur, mais améliore énormément la qualité, la compréhension et la reprise du code par une tierce personne.
Cela implique aussi de \textbf{tout} documenter: les modules, les paquets, les classes, les fonctions, méthodes, ...
Ce qui peut également aller à contrecourant d'autres pratiques \cite{clean_code}{53-74} ; il y a une juste mesure à prendre entre "tout documenter" et "tout bien documenter":
Ceci impose une certaine rigueur, tout en améliorant énormément la qualité, la compréhension et la reprise du code par une tierce personne.
Ceci implique aussi de \textbf{tout} documenter: les modules, les paquets, les classes, les fonctions, méthodes, ... ce qui peut aller à contrecourant d'autres pratiques \cite{clean_code}{53-74} ; il y a donc une juste mesure à prendre entre "tout documenter" et "tout bien documenter":
\begin{itemize}
@ -215,27 +206,21 @@ Ce qui peut également aller à contrecourant d'autres pratiques \cite{clean_cod
\item
Inutile de décrire quelque chose qui est évident; documenter la méthode \mintinline{python}{get_age()} d'une personne n'aura pas beaucoup d'intérêt
\item
S'il est nécessaire de décrire un comportement au sein-même d'une
fonction, c'est que ce comportement pourrait être extrait dans une
nouvelle fonction (qui, elle, pourra être documentée)
S'il est nécessaire de décrire un comportement au sein-même d'une fonction, avec un commentaire \emph{inline}, c'est que ce comportement pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être documentée proprement
\end{itemize}
Documentation: be obsessed! Mais \textbf{le code reste la référence}
Il existe plusieurs types de conventions de documentation:
Il existe plusieurs types de balisages reconnus/approuvés:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
PEP 257
\item
Numpy
\item
Google Style (parfois connue sous l'intitulé \texttt{Napoleon})
\item
\ldots\hspace{0pt}
\item RestructuredText
\item Numpy
\item Google Style (parfois connue sous l'intitulé \texttt{Napoleon})
\end{enumerate}
... mais tout système de balisage peut être reconnu, sous réseve de respecter la structure de la PEP257.
\subsection{PEP 257}
La \href{https://peps.python.org/pep-0257/#what-is-a-docstring}{PEP-257} nous donne des recommandations haut-niveau concernant la structure des docstrings: ce qu'elles doivent contenir et comment l'expliciter, sans imposer quelle que mise en forme que ce soit.
@ -344,7 +329,7 @@ générer automatiquement le squelette de documentation d'un bloc de code:
Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis l'interface d'administration.
Toute information pertinente peut donc lier le code à un cas d'utilisation concret, et rien n'est jamais réellement perdu.
\section{Vérification du code (lint)\index{lint}}
\section{Vérification du code \index{lint}}
Il existe plusieurs niveaux de \emph{linters}:
@ -434,8 +419,7 @@ Nous trouvons des erreurs:
votre application se prendra méchamment les pieds dans le tapis).
\end{itemize}
L'étape d'après consiste à invoquer pylint. Lui, il est directement
moins conciliant:
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
\begin{verbatim}
$ pylint test.py
@ -460,9 +444,8 @@ test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
Your code has been rated at -5.45/10
\end{verbatim}
En gros, j'ai programmé comme une grosse bouse anémique (et oui: le
score d'évaluation du code permet bien d'aller en négatif). En vrac,
nous trouvons des problèmes liés:
En gros, j'ai programmé comme une grosse bouse anémique (et oui: le score d'évaluation du code permet d'aller en négatif).
En vrac, nous trouvons des problèmes liés:
\begin{itemize}
\item
@ -477,48 +460,57 @@ nous trouvons des problèmes liés:
Pour reprendre la
\href{http://pylint.pycqa.org/en/latest/user_guide/message-control.html}{documentation},
chaque code possède sa signification (ouf!):
chaque code possède sa signification:
\begin{itemize}
\item
C convention related checks
\textbf{C}, pour toutes les vérifications liées aux conventions (nommage et mises en forme, que l'on a vues ci-dessus)
\item
R refactoring related checks
\textbf{R}, pour des propositions de refactoring
\item
W various warnings
\textbf{W}, pour tout ce qui est en lien avec des avertissements
\item
E errors, for probable bugs in the code
\textbf{E} pour les erreurs ou des bugs probablement présents dans le code
\item
F fatal, if an error occurred which prevented pylint from doing
further* processing.
\textbf{F} pour les erreurs internes au fonctionnement de pylint, qui font que le traitement n'a pas pu aboutir.
\end{itemize}
TODO: Expliquer comment faire pour tagger une explication.
Connaissant ceci, il est extrêmement pratique d'intégrer pylint au niveau des processus d'intégration continue, puisque la présence d'une
Pylint propose également une option particulièrement efficace, qui prend le paramètre \texttt{--errors-only}, et qui n'affiche que les occurrences appartenant à la catégorie \textbf{E}.
TODO: Voir si la sortie de pylint est obligatoirement 0 s'il y a un
warning
Si nous souhaitons ignorer l'une de ces catégories, ce doit être fait explicitement: de cette manière, nous marquons notre approbation pour que pylint ignore consciemment un élément en particulier.
Cet élément peut être:
\begin{enumerate}
\item \textbf{Une ligne de code}
\item \textbf{Un bloc de code} - une fonction, une méthode, une classe, un module, ...
\item \textbf{Un projet entier}, en spécifiant la non-prise en compte au niveau du fichier \texttt{.pylintrc}, qui contient
\end{enumerate}
\subsection{Ignorer une ligne de code}
\subsection{Ignorer un bloc de code}
\subsection{Ignorer une catégorie globalement}
TODO: parler de \texttt{pylint\ -\/-errors-only}
\section{Formatage de code}
Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de
style de rédaction pour la documentation (PEP257), d'un \emph{linter}
pour nous indiquer quels morceaux de code doivent absolument être revus,
\ldots\hspace{0pt} Reste que ces tâches sont parfois (très) souvent
fastidieuses: écrire un code propre et systématiquement cohérent est une
tâche ardue. Heureusement, il existe des outils pour nous aider (un
peu).
Nous avons parlé ci-dessous de style de codage pour Python (PEP8), de style de rédaction pour la documentation (PEP257), d'un vérificateur pour nous indiquer quels morceaux de code doivent absolument être revus, ...
Reste que ces tâches sont parfois (très) souvent fastidieuses: écrire un code propre et systématiquement cohérent est une tâche ardue.
Heureusement, il existe plusieurs outils pour nous aider au niveau du formatage automatique.
Même si elle n'est pas parfaite, la librairie \href{https://black.readthedocs.io/en/stable/}{Black} arrive à un très bon compromis entre
A nouveau, il existe plusieurs possibilités de formatage automatique du
code. Même si elle n'est pas parfaite,
\href{https://black.readthedocs.io/en/stable/}{Black} arrive à un
compromis entre clarté du code, facilité d'installation et d'intégration
et résultat.
\begin{itemize}
\item Clarté du code
\item Facilité d'installation et d'intégration
\item Résultat
\end{itemize}
Est-ce que ce formatage est idéal et accepté par tout le monde ?
\textbf{Non}. Même Pylint arrivera parfois à râler. Mais ce formatage
conviendra dans 97,83\% des cas (au moins).
\textbf{Non}.
Même Pylint arrivera parfois à râler.
Mais ce formatage conviendra dans 97,83\% des cas (au moins).
\begin{quote}
By using Black, you agree to cede control over minutiae of
@ -532,13 +524,182 @@ Formatting becomes transparent after a while and you can focus on the
content instead.
\end{quote}
Traduit rapidement à partir de la langue de Batman: "\emph{En utilisant
Traduit rapidement à partir de la langue de Batman: "\texttt{En utilisant
Black, vous cédez le contrôle sur le formatage de votre code. En retour,
Black vous fera gagner un max de temps, diminuera votre charge mentale
et fera revenir l'être aimé}". Mais la partie réellement intéressante
concerne le fait que "\emph{Tout code qui sera passé par Black aura la
concerne le fait que "\texttt{Tout code qui sera passé par Black aura la
même forme, indépendamment du project sur lequel vous serez en train de
travailler. L'étape de formatage deviendra transparente, et vous pourrez
vous concentrer sur le contenu}".
\section{Typage statique \index{PEP585}}
Nous vous disions ci-dessus que Python est un langage dynamique interprété.
Concrètement, cela signifie que des erreurs qui auraient pu avoir été détectées lors de la phase de compilation, ne le sont pas avec Python.
Il existe cependant une solution à ce problème, sous la forme de \href{http://mypy-lang.org/}{Mypy}, qui peut vérifier une forme de typage statique de votre code source, grâce à une expressivité du code, basée sur des annotations.
Ces vérifications se présentent de la manière suivante:
\begin{listing}[H]
\begin{minted}{python}
from typing import List
def first_int_elem(l: List[int]) -> int:
return l[0] if l else None
if __name__ == "__main__":
print(first_int_elem([1, 2, 3]))
print(first_int_elem(['a', 'b', 'c']))
\end{minted}
\end{listing}
Est-ce que le code ci-dessous fonctionne correctement ? \textbf{Oui}:
\begin{listing}[H]
\begin{verbatim}
>>> python mypy-test.py
1
a
\end{verbatim}
\end{listing}
Malgré que nos annotations déclarent une liste d'entiers, rien ne nous empêche de lui envoyer une liste de caractères, sans que cela ne lui pose de problèmes.
La signature de notre fonction n'est donc pas cohérente avec son comportement.
Est-ce que Mypy va râler ? \textbf{Oui, aussi}.
Non seulement nous retournons la valeur \texttt{None} si la liste est vide alors que nous lui annoncions un entier en sortie, mais en plus, nous l'appelons avec une liste de caractères, alors que nous nous attendons à une liste d'entiers:
\begin{listing}[H]
\begin{verbatim}
>>> mypy mypy-test.py
mypy-test.py:7: error: Incompatible return value type (got "Optional[int]", expected "int")
mypy-test.py:12: error: List item 0 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 1 has incompatible type "str"; expected "int"
mypy-test.py:12: error: List item 2 has incompatible type "str"; expected "int"
Found 4 errors in 1 file (checked 1 source file)
\end{verbatim}
\end{listing}
Pour corriger ceci, nous devons:
\begin{enumerate}
\item
Importer le type \texttt{Optional} et l'utiliser en sortie de notre
fonction \texttt{first\_int\_elem}
\item
Eviter de lui donner de mauvais paramètres ;-)²
\end{enumerate}
\section{Tests unitaires}
Comme tout bon \textbf{langage de programmation moderne} qui se respecte, Python embarque tout un environnement facilitant le lancement de tests;
Une bonne pratique (parfois discutée) consiste cependant à switcher vers \texttt{pytest}, qui présente quelques avantages par rapport au module \texttt{unittest}:
\begin{itemize}
\item
Une syntaxe plus concise (au prix de \href{https://docs.pytest.org/en/reorganize-docs/new-docs/user/naming_conventions.html}{quelques conventions}, même si elles restent configurables): un test est une fonction, et ne doit pas obligatoirement faire partie d'une classe héritant de \texttt{TestCase} - la seule nécessité étant que cette fonction fasse partie d'un module commençant ou finissant par "test" (\texttt{test\_example.py} ou \texttt{example\_test.py}).
\item
Une compatibilité avec du code Python "classique" - vous ne devrez
donc retenir qu'un seul ensemble de commandes ;-)
\item
Des \emph{fixtures} faciles à réutiliser entre vos différents
composants
\item
Une compatibilité avec le reste de l'écosystème, dont la couverture de
code présentée ci-dessous.
\end{itemize}
Ainsi, après installation, il nous suffit de créer notre module
\texttt{test\_models.py}, dans lequel nous allons simplement tester
l'addition d'un nombre et d'une chaîne de caractères (oui, c'est
complètement biesse; on est sur la partie théorique ici):
\begin{listing}
\begin{minted}{Python}
def test_add():
assert 0 + 0 == "La tête à Toto"
\end{minted}
\end{listing}
Forcément, cela va planter.
Pour nous en assurer (dès fois que quelqu'un en doute), il nous suffit de démarrer la commande \texttt{pytest}:
\begin{listing}
\begin{verbatim}
pytest
================= test session starts =================
platform ...
rootdir: ...
plugins: django-4.1.0
collected 1 item
gwift\test_models.py F
[100%]
================= FAILURES =================
_________________ test_basic_add _________________
def test_basic_add():
> assert 0 + 0 == "La tête à Toto"
E AssertionError: assert (0 + 0) == 'La tête à Toto'
tests.py:2: AssertionError
================= short test summary info =================
FAILED tests.py::test_basic_add - AssertionError: assert (0 + 0) == 'La tête à Toto'
================= 1 failed in 0.10s =================
\end{verbatim}
\end{listing}
\subsection{Couverture de code}
Avec \texttt{pytest}, il convient d'utiliser le paquet
\href{https://pypi.org/project/pytest-cov/}{\texttt{pytest-cov}}, suivi
de la commande \texttt{pytest\ -\/-cov=gwift\ tests/}.
Si vous préférez rester avec le cadre de tests de Django, vous pouvez
passer par le paquet
\href{https://pypi.org/project/django-coverage-plugin/}{django-coverage-plugin}
Ajoutez-le dans le fichier \texttt{requirements/base.txt}, et lancez une
couverture de code grâce à la commande \texttt{coverage}. La
configuration peut se faire dans un fichier \texttt{.coveragerc} que
vous placerez à la racine de votre projet, et qui sera lu lors de
l'exécution.
\section{Matrice de compatibilité}
L'intérêt de la matrice de compatibilité consiste à spécifier un ensemble de plusieurs versions d'un même interpréteur (ici, Python), afin de s'assurer que votre application continue à fonctionner.
Nous sommes donc un cran plus haut que la spécification des versions des librairies, puisque nous nous situons directement au niveau de l'interpréteur.
L'outil le plus connu est \href{https://tox.readthedocs.io/en/latest/}{Tox}, qui consiste en un outil basé sur virtualenv et qui permet:
\begin{enumerate}
\item De vérifier que votre application s'installe correctement avec différentes versions de Python et d'interpréteurs
\item De démarrer des tests parmi ces différents environnements
\end{enumerate}
\begin{listing}
\begin{verbatim}
# content of: tox.ini , put in same dir as setup.py
[tox]
envlist = py36,py37,py38,py39
skipsdist = true
[testenv]
deps =
-r requirements/dev.txt
commands =
pytest
\end{verbatim}
\end{listing}
Démarrez ensuite la commande \texttt{tox}, pour démarrer la commande \texttt{pytest} sur les environnements Python 3.6, 3.7, 3.8 et 3.9, après avoir installé nos dépendances présentes dans le fichier \texttt{requirements/dev.txt}.
Pour que la commande ci-dessus fonctionne correctement, il sera nécessaire que vous ayez les différentes versions d'interpréteurs installées.
Ci-dessus, la commande retournera une erreur pour chaque version non trouvée, avec une erreur type
\texttt{ERROR:\ \ \ pyXX:\ InterpreterNotFound:\ pythonX.X}.
TODO: Intérêt des containers.
\section{Conclusions (et intégration continue)}
Mypy + black + pylint + flake8 + pyflakes + ...

View File

@ -114,4 +114,21 @@ Une solution serait de passer par un dictionnaire, de façon à ramener la compl
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc.
Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants.
Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante.
Il existe plusieurs types de tests (intégration, comportement, ...)
Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux.
C'est ici qu'il est utile d'avoir le pourcentage de code couvert par les différents tests, pour savoir ce qui peut être amélioré, le but du jeu consistant simplement à augmenter ou égaler le pourcentage de couverture de code existant avant chaque modification.
Gitlab permet de visualiser cette information de manière très propre, en l'affichant au niveau de chaque proposition d'intégration.
La couverture de code est une analyse qui donne un pourcentage lié à la
quantité de code couvert par les tests. Attention qu'il ne s'agit pas de
vérifier que le code est \textbf{bien} testé, mais juste de vérifier
\textbf{quelle partie} du code est testée. Le paquet \texttt{coverage}
se charge d'évaluer le pourcentage de code couvert par les tests.
\section{Tests d'intégration}

View File

@ -52,7 +52,7 @@
\include{chapters/python.tex}
\chapter{Démarrer un nouveau projet}
\include{chapters/new-project.tex}
\part{Principes fondamentaux de Django}