gwift-book/chapters/python.tex

1456 lines
54 KiB
TeX
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\chapter{Le langage Python}
Le langage \href{https://www.python.org/}{Python} est un \href{https://docs.python.org/3/faq/general.html\#what-is-python}{langage de programmation} interprété, interactif, amusant, orienté objet (souvent), fonctionnel (parfois), open source, multi-plateformes, flexible, facile à apprendre et difficile à maîtriser.
Il a été développé et publié en 1991 pour Guido van Rossum, qui \textit{cherchait à créer un langage amateur pour s'occuper le week-end et durant les fêtes de Noël} \footnote{\url{https://www.python.org/doc/essays/foreword}}.
Le projet a décollé et Python est maintenant considéré comme l'un des langages de programmation les plus populaires.
\begin{figure}[!ht]
\centering
\scalebox{.8}{\includegraphics[max size={\textwidth}{\textheight}]{images/xkcd-353-python.png}}
\caption{\url{https://xkcd.com/353/}}
\end{figure}
Le langage est géré en partie par la \textit{Python Software Foundation}, créée en 2001, et en partie par différents sponsors, dont HPE, Intel et Google. \footnote{De son côté, Django est soutenu par la \href{https://www.djangoproject.com/foundation/}{Django Foundation}. Elle est alimentée par des volontaires et recevait (en 2013) moins de \$50 000 en donations directes. \cite[p. 112]{roads_and_bridges}}
A première vue ou suivants les langages que vous connaitriez ou auriez déjà abordé, certains concepts restent difficiles à aborder:
\begin{itemize}
\item
L'indentation définit l'étendue d'un bloc (classe, fonction, méthode, boucle, condition),
\item
Il n'y a pas de typage fort des variables
\item
Le compilateur n'est pas là pour assurer le filet de sécurité avant la mise en production (puisqu'il n'y a pas de compilateur)
\end{itemize}
Malgré ces quelques points, Python reste un langage généraliste accessible et "bon partout", et peut se reposer sur un écosystème stable et fonctionnel, qui tourne grâce avec un système d'améliorations basées sur des propositions: les PEP, ou "\textbf{Python Enhancement Proposal}\index{PEP}".
Chacune d'entre elles doit être approuvée par le \href{http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life}{Benevolent Dictator For Life\index{BDFL}}.
Le langage Python utilise un typage dynamique appelé \href{https://fr.wikipedia.org/wiki/Duck_typing}{\textbf{duck typing}}:
\begin{quote}
"\emph{When I see a bird that quacks like a duck, walks like a duck, has
feathers and webbed feet and associates with ducks --- I'm certainly
going to assume that he is a duck}" \footnote{\url{http://en.wikipedia.org/wiki/Duck_test}}
\end{quote}
En fonction de votre niveau d'apprentissage du langage, plusieurs ressources pourraient vous aider:
\begin{itemize}
\item
\textbf{Pour les débutants}, \href{https://automatetheboringstuff.com/}{Automate the Boring Stuff with Python} \cite{boring_stuff}, aka. \emph{Practical Programming for Total Beginners}
\item
\textbf{Pour un (gros) niveau au dessus} et pour un état de l'art du langage, nous ne pouvons que vous recommander le livre Expert Python Programming \cite{expert_python}, qui aborde énormément d'aspects du langage en détails (mais pas toujours en profondeur): les différents types d'interpréteurs, les éléments de langage avancés, différents outils de productivité, métaprogrammation, optimisation de code, programmation orientée évènements, multithreading et concurrence, tests, \ldots
A ce jour, c'est le concentré de sujets liés au langage le plus intéressant qui ait pu arriver entre nos mains.
\end{itemize}
En parallèle, si vous avez besoin d'un aide-mémoire ou d'une liste exhaustive des types et structures de données du langage, référez-vous au lien suivant: \href{https://gto76.github.io/python-cheatsheet/}{Python Cheat Sheet}.
\section{Protocoles de langage}
Le modèle de données du langage spécifie un ensemble de méthodes qui peuvent être surchargées.
Ces méthodes suivent une convention de nommage et leur nom est toujours encadré par un double tiret souligné; d'où leur nom de "\emph{dunder methods}\index{dunder}" ou "\emph{double-underscore methods}".
La méthode la plus couramment utilisée est la méthode \texttt{init()}, qui permet de surcharger l'initialisation d'une instance de classe.
\begin{listing}[!ht]
\begin{minted}{python}
class CustomUserClass:
def __init__(self, initiatization_argument):
...
\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}.
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.
Vous trouverez ci-dessous un tableau reprenant les protocoles les plus courants:
\begin{center}
\begin{tabular}{ c c c }
cell1 & cell2 & cell3 \\
cell4 & cell5 & cell6 \\
cell7 & cell8 & cell9
\end{tabular}
\end{center}
Les points principaux à présenter ci-dessus:
\begin{enumerate}
\item \_\_lt\_\_, \_\_gt\_\_
\item item()
\item getitem() - pour utiliser les []
\item len() - pour connaître la longueur d'un objet
\item \ldots
\end{enumerate}
Le langage autorise nativement plus d'une cinquantaine d'opérateurs différents:
\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 \ldots
\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}[tabsize=4]{python}
def __sub__(self, other):
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)
]
)
\end{minted}
\end{listing}
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.
\section{Structures de données}
Le langage Python est un langage orienté objet au sens où \textbf{tout} est un objet.
Cela signifie qu'il ne se limite pas à modéliser une structure de données, mais qu'il embarque son propre comportement, avec ses méthodes.
Les principales structures de données sont :
\begin{itemize}
\item Les listes (y inclus les tableaux) et tuples
\item Les dictionnaires (y inclus les \texttt{maps})
\item Les \textit{namedtuples} et \textit{dataclasses}
\item Les classes
\end{itemize}
\subsection{Dictionnaires}
Au hasard des internets multimédias, on est tombé sur un morceau de code à base de \texttt{pop} et de réinitialisation.
Quelque chose comme ceci:
\begin{minted}{python}
width = OPT_CMD_ARGS.pop(width_arg_name)
heigth = OPT_CMD_ARGS.pop(heigth_arg_name)
args_list.append(width)
args_list.append(heigth)
OPT_CMD_ARGS[width_arg_name] = width
OPT_CMD_ARGS[heigth_arg_name] = heigth
\end{minted}
Techniquement, un dictionnaire est mutable et peut donc être modifié.
Dans ce cas-ci, le dictionnaire sert à récupérer l'association entre un nom de paramètre et sa valeur pour la ligne de commande.
Le `pop` est donc utilisé uniquement pour récupérer une valeur: dans ce cas, il est possible d'utiliser la méthode `get`, qui permettra de définir une valeur par défaut:
\begin{minted}{python}
>>> canevas = dict() # on aurait aussi pu déclarer le dictionnaire avec canevas = {}
>>> canevas["a"] # avec un accès direct, si la clé n'existe pas, une exception est levée
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'a'
>>> canevas.get("a", "abracadabra") # par contre, si on utilise get, aucune exception n'est levée
'abracadabra'
>>> canevas
{}
\end{minted}
Y'a une méthode du fieu de dieu: `setdefault` !
en gros, ça te retourne la valeur de la clé si elle existe, et si pas, cela te crée une nouvelle clé et te la retourne:
\begin{minted}{python}
>>> d = {}
>>> d.setdefault(1, "Yolo")
'Yolo'
>>> d
{1: 'Yolo'}
\end{minted}
Ca évite, dans une boucle, de vérifier si la clé existe déjà, et si pas, de la créer
\section{Introspection}
\begin{minted}{python}
def on_command_1():
print("Oune")
def on_command_2():
print("Dos")
def on_command_3():
print("Tresse")
def prrrt():
print("Won't be printed")
\end{minted}
Tu peux retrouver les trois méthodes \texttt{on\_command\_*}` via la fonction `dir()`:
\begin{minted}{python}
>>> import module_chelou
>>> dir(module_chelou)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'on_command_1', 'on_command_2', 'on_command_3', 'prrrt']
>>> for function_name in [x for x in dir(module_chelou) if "on_command_" in x]:
... getattr(module_chelou, function_name)()
...
Oune
Dos
Tresse
```
\end{minted}
\section{The Zen of Python}
(aussi connue sous PEP20 ;)) \index{PEP!PEP20}
\begin{listing}[H]
\begin{verbatim}
>>> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!
\end{verbatim}
\caption{The Zen of Python}
\end{listing}
\section{Guide de style}
La première PEP qui va nous intéresser est la PEP8. \index{PEP!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, \ldots
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é.
Comme l'indique la PEP20, \textit{Readibility counts}.
Ceci implique de garder une cohérence dans l'écriture du code, dont les principales recommandations concernent:
\begin{enumerate}
\item
\textbf{Le layout général} du code: indentation, longueur de ligne, séparateurs de lignes, encodage des fichiers sources et gestion des imports.
\item
\textbf{La gestion des commentaires}, en fonction de leur emplacement: blocs de commentaires, commentaires embarqués ou documentation.
\item
\textbf{Les conventions de nommage}: les noms à éviter, comment nommer une classe, une fonction ou une variable, un paquet, les exceptions ou les constantes.
\item
\textbf{Des recommandations de programmation}, concernant le typage statique \index{PEP!PEP484}
\end{enumerate}
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}, \ldots).
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}
$ 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
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
\end{verbatim}
\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}
Il y a une énorme différence entre la documentation du code et les commentaires associés au code:
\begin{enumerate}
\item \textbf{Les commentaires} décrivent comment le code fonctionne
\item \textbf{La documentation du code} décrit comment utiliser le code.
\end{enumerate}
Pour que le code soit utilisé correctement, il est dès lors important de documenter :
\begin{enumerate}
\item
Ce que représente une classe ou une API
\item
Les attentes d'une interface : codes d'erreurs, statuts possibles, etc.
\end{enumerate}
Les commentaires sont cependant utiles lorsque le code n'est pas évident à lire ou lorsque certaines optimisations auraient été appliquées dans un soucis de performances.
Dans la majorité des cas cependant, si des commentaires doivent être rédigés pour que le code devienne lisible, c'est que ce code n'est pas correctement écrit. \cite{clean_code}{53-74} \footnote{\url{https://www.youtube.com/watch?v=Bf7vDBBOBUA}}
Nous pouvons constater un risque de décorrélation entre le code et ses commentaires :
\begin{listing}[!ht]
\begin{minted}[tabsize=4]{python}
# Check that the maximum length of the array if less than 8
if len(array) < 10:
...
\end{minted}
\end{listing}
Il y a donc une juste mesure à prendre entre "tout documenter" (les modules, les paquets, les classes, les fonctions, méthodes, \ldots) et "bien documenter":
\begin{itemize}
\item
Inutile d'ajouter des watermarks, auteurs, \ldots
Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois,
\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, 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}
En résumé, vous pouvez être obsédé par la documentation, mais \textbf{le code reste la référence}.
Il existe plusieurs types de balisages reconnus/approuvés:
\begin{enumerate}
\item RestructuredText
\item Numpy
\item Google Style (parfois connue sous l'intitulé \texttt{Napoleon})
\end{enumerate}
\ldots mais tout système de balisage peut être reconnu, sous réseve de respecter la structure de la PEP257.
\subsection{PEP 257} \index{PEP!PEP257}
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.
de contenu, mais pas de forme, notamment sur la manière de représenter des docstrings ne nécessitant qu'une seule ligne, nécessitant plusieurs lignes ou de gérer l'indentation.
Son objectif est d'arriver à une forme de cohérence lorsqu'un utilisateur souhaitera accéder à la propriété
Elle contient des conventions, pas des règles ou
\begin{quote}
“A universal convention supplies all of maintainability, clarity, consistency, and a foundation for good programming habits too. What it doesnt do is insist that you follow it against your will. Thats Python!”
-- Tim Peters on comp.lang.python, 2001-06-16
\end{quote}
Ainsi, les conventions sont décrites; chaque format propose ensuite son propre balisage (ReStructuredText, Numpy, Napoleon, \ldots).
A priori, vous pourriez tout à fait utiliser le vôtre, sous réserve que les conventions de la PEP-257 soient respectées.
\subsection{RestructuredText}
\href{https://peps.python.org/pep-0287/}{See also ;)}
\href{https://peps.python.org/pep-0258/}{See also}
\subsection{Numpy}
\href{https://numpydoc.readthedocs.io/en/latest/format.html}{Numpy} sépare chaque section avec un pseudo-formatage de titre.
Les sections sont définies selon les types suivants:
\begin{itemize}
\item
Une courte description (de la classe, du module, de la fonction, \ldots).
Chaque module devrait avoir une docstring au tout début du fichier.
Cette docstring peut s'étendre sur plusieurs lignes (\textit{Short summary})
\item
Des avertissements liés à la dépréciation: quand la fonctionnalité viendra à disparaitre (dans quelle version), les raisons de sa dépréciation, et les recommandations pour arriver au même résultat. (\textit{Deprecated})
\item
Une description plus précise des fonctionnalités proposées (et pas de la manière dont l'implémentation est réalisée). (\textit{Extended summary})
\item
Les paramètres attendus, leur type et une description (\textit{Parameters})
\item
La ou les valeurs de retour, accompagnées de leur type et d'une description (\textit{Returns})
\item
Les valeurs générées (\textit{yields}) dans le cas de générateurs (\textit{Yields})
\item
Un objet attendu par la méthode \texttt{send()} (toujours dans le cas de générateurs) (\textit{Receives})
\item
D'autres paramètres, notamment dans le cas des paramètres \texttt{*args} et \texttt{**kwargs}. (\textit{Other parameters})
\item
Les exceptions levées (\textit{Raises})
\item
Les avertissements destinés à l'utilisateur (\textit{Warnings})
\item
Une section "Pour continuer\ldots" (\textit{See also})
\item Des notes complémentaires (\textit{Notes})
\item Des références (\textit{References})
\item Des exemples (\textit{Examples})
\end{itemize}
\begin{listing}[!ht]
\begin{minted}[tabsize=4]{python}
"""Docstring for the example.py module.
Modules names should have short, all-lowercase names. The module name may
have underscores if this improves readability.
"""
import os
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
def foo(var1, var2, *args, long_var_name="hi", only_seldom_used_keyword=0, **kwargs):
r"""Summarize the function in one line.
Several sentences providing an extended description. Refer to
variables using back-ticks, e.g. `var`.
Parameters
----------
<parameter_name> : <parameter_type>
<parameter_description>
Returns
-------
type
Explanation of anonymous return value of type ``type``.
describe : type
Explanation of return value named `describe`.
out : type
Explanation of `out`.
type_without_description
Other Parameters
----------------
only_seldom_used_keyword : int, optional
Infrequently used parameters can be described under this optional
section to prevent cluttering the Parameters section.
**kwargs : dict
Other infrequently used keyword arguments. Note that all keyword
arguments appearing after the first parameter specified under the
Other Parameters section, should also be described under this
section.
Raises
------
BadException
Because you shouldn't have done that.
See Also
--------
numpy.array : Relationship (optional).
numpy.ndarray : Relationship (optional), which could be fairly long, in
which case the line wraps here.
numpy.dot, numpy.linalg.norm, numpy.eye
Notes
-----
Notes about the implementation algorithm (if needed).
This can have multiple paragraphs.
You may include some math:
.. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n}
And even use a Greek symbol like :math:`\omega` inline.
References
----------
Cite the relevant literature, e.g. [1]_. You may also cite these
references in the notes section above.
.. [1] O. McNoleg, "The integration of GIS, remote sensing,
expert systems and adaptive co-kriging for environmental habitat
modelling of the Highland Haggis using object-oriented, fuzzy-logic
and neural-network techniques," Computers & Geosciences, vol. 22,
pp. 585-588, 1996.
Examples
--------
These are written in doctest format, and should illustrate how to
use the function.
>>> a = [1, 2, 3]
>>> print([x + 3 for x in a])
[4, 5, 6]
>>> print("a\nb")
a
b
"""
pass
\end{minted}
\caption{Un exemple de documentation Numpy}
\end{listing}
\subsection{Napoleon}
Les \href{https://google.github.io/styleguide/pyguide.html\#38-comments-and-docstrings}{conventions proposées par Google} nous semblent plus faciles à lire que du RestructuredText, mais sont parfois moins bien intégrées que les docstrings officiellement supportées (par exemple, \href{https://clize.readthedocs.io/en/stable/}{clize} ne reconnait que du RestructuredText; \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{l'auto-documentation} de Django également).
L'exemple donné dans les guides de style de Google est celui-ci:
\begin{listing}[!ht]
\begin{minted}[tabsize=4]{python}
def fetch_smalltable_rows(
table_handle: smalltable.Table,
keys: Sequence[Union[bytes, str]],
require_all_keys: bool = False,
) -> Mapping[bytes, Tuple[str]]:
"""Fetches rows from a Smalltable.
Retrieves rows pertaining to the given keys from the Table instance
represented by table_handle. String keys will be UTF-8 encoded.
Args:
table_handle: An open smalltable.Table instance.
keys: A sequence of strings representing the key of each table
row to fetch. String keys will be UTF-8 encoded.
require_all_keys: Optional; If require_all_keys is True only
rows with values set for all keys will be returned.
Returns:
A dict mapping keys to the corresponding table row data
fetched. Each row is represented as a tuple of strings. For
example:
{
b'Serak': ('Rigel VII', 'Preparer'),
b'Zim': ('Irk', 'Invader'),
b'Lrrr': ('Omicron Persei 8', 'Emperor')
}
Returned keys are always bytes. If a key from the keys argument is
missing from the dictionary, then that row was not found in the
table (and require_all_keys must have been False).
Raises:
IOError: An error occurred accessing the smalltable.
"""
\end{minted}
\caption{Un exemple de documentation Napoleon}
\end{listing}
C'est-à-dire:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Une courte ligne d'introduction, descriptive, indiquant ce que la
fonction ou la méthode réalise. Attention, la documentation ne doit
pas indiquer \emph{comment} la fonction/méthode est implémentée, mais
ce qu'elle fait concrètement (et succintement).
\item
Une ligne vide
\item
Une description plus complète et plus verbeuse, si vous le jugez
nécessaire
\item
Une ligne vide
\item
La description des arguments et paramètres, des valeurs de retour, des
exemples et les exceptions qui peuvent être levées.
\end{enumerate}
Un exemple (encore) plus complet peut être trouvé
\href{https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html\#example-google}{dans
le dépôt sphinxcontrib-napoleon}. Et ici, nous tombons peut-être dans
l'excès de zèle:
\begin{listing}[!ht]
\begin{minted}[tabsize=4]{python}
# -*- coding: utf-8 -*-
"""Example Google style docstrings.
This module demonstrates documentation as specified by the `Google Python
Style Guide`_. Docstrings may extend over multiple lines. Sections are created
with a section header and a colon followed by a block of indented text.
Example:
Examples can be given using either the ``Example`` or ``Examples``
sections. Sections support any reStructuredText formatting, including
literal blocks::
$ python example_google.py
Section breaks are created by resuming unindented text. Section breaks
are also implicitly created anytime a new section starts.
Attributes:
module_level_variable1 (int): Module level variables may be documented in
either the ``Attributes`` section of the module docstring, or in an
inline docstring immediately following the variable.
Either form is acceptable, but the two should not be mixed. Choose
one convention to document module level variables and be consistent
with it.
Todo:
* For module TODOs
* You have to also use ``sphinx.ext.todo`` extension
.. _Google Python Style Guide:
http://google.github.io/styleguide/pyguide.html
"""
module_level_variable1 = 12345
module_level_variable2 = 98765
"""int: Module level variable documented inline.
The docstring may span multiple lines. The type may optionally be specified
on the first line, separated by a colon.
"""
def function_with_types_in_docstring(param1, param2):
"""Example function with types documented in the docstring.
`PEP 484`_ type annotations are supported. If attribute, parameter, and
return types are annotated according to `PEP 484`_, they do not need to be
included in the docstring:
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
Returns:
bool: The return value. True for success, False otherwise.
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484/
"""
def function_with_pep484_type_annotations(param1: int, param2: str) -> bool:
"""Example function with PEP 484 type annotations.
Args:
param1: The first parameter.
param2: The second parameter.
Returns:
The return value. True for success, False otherwise.
"""
def module_level_function(param1, param2=None, *args, **kwargs):
"""This is an example of a module level function.
Function parameters should be documented in the ``Args`` section. The name
of each parameter is required. The type and description of each parameter
is optional, but should be included if not obvious.
If \*args or \*\*kwargs are accepted,
they should be listed as ``*args`` and ``**kwargs``.
The format for a parameter is::
name (type): description
The description may span multiple lines. Following
lines should be indented. The "(type)" is optional.
Multiple paragraphs are supported in parameter
descriptions.
Args:
param1 (int): The first parameter.
param2 (:obj:`str`, optional): The second parameter. Defaults to None.
Second line of description should be indented.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
Returns:
bool: True if successful, False otherwise.
The return type is optional and may be specified at the beginning of
the ``Returns`` section followed by a colon.
The ``Returns`` section may span multiple lines and paragraphs.
Following lines should be indented to match the first line.
The ``Returns`` section supports any reStructuredText formatting,
including literal blocks::
{
'param1': param1,
'param2': param2
}
Raises:
AttributeError: The ``Raises`` section is a list of all exceptions
that are relevant to the interface.
ValueError: If `param2` is equal to `param1`.
"""
if param1 == param2:
raise ValueError('param1 may not be equal to param2')
return True
def example_generator(n):
"""Generators have a ``Yields`` section instead of a ``Returns`` section.
Args:
n (int): The upper limit of the range to generate, from 0 to `n` - 1.
Yields:
int: The next number in the range of 0 to `n` - 1.
Examples:
Examples should be written in doctest format, and should illustrate how
to use the function.
>>> print([i for i in example_generator(4)])
[0, 1, 2, 3]
"""
for i in range(n):
yield i
class ExampleError(Exception):
"""Exceptions are documented in the same way as classes.
The __init__ method may be documented in either the class level
docstring, or as a docstring on the __init__ method itself.
Either form is acceptable, but the two should not be mixed. Choose one
convention to document the __init__ method and be consistent with it.
Note:
Do not include the `self` parameter in the ``Args`` section.
Args:
msg (str): Human readable string describing the exception.
code (:obj:`int`, optional): Error code.
Attributes:
msg (str): Human readable string describing the exception.
code (int): Exception error code.
"""
def __init__(self, msg, code):
self.msg = msg
self.code = code
class ExampleClass(object):
"""The summary line for a class docstring should fit on one line.
If the class has public attributes, they may be documented here
in an ``Attributes`` section and follow the same formatting as a
function's ``Args`` section. Alternatively, attributes may be documented
inline with the attribute's declaration (see __init__ method below).
Properties created with the ``@property`` decorator should be documented
in the property's getter method.
Attributes:
attr1 (str): Description of `attr1`.
attr2 (:obj:`int`, optional): Description of `attr2`.
"""
def __init__(self, param1, param2, param3):
"""Example of docstring on the __init__ method.
The __init__ method may be documented in either the class level
docstring, or as a docstring on the __init__ method itself.
Either form is acceptable, but the two should not be mixed. Choose one
convention to document the __init__ method and be consistent with it.
Note:
Do not include the `self` parameter in the ``Args`` section.
Args:
param1 (str): Description of `param1`.
param2 (:obj:`int`, optional): Description of `param2`. Multiple
lines are supported.
param3 (:obj:`list` of :obj:`str`): Description of `param3`.
"""
self.attr1 = param1
self.attr2 = param2
self.attr3 = param3 #: Doc comment *inline* with attribute
#: list of str: Doc comment *before* attribute, with type specified
self.attr4 = ['attr4']
self.attr5 = None
"""str: Docstring *after* attribute, with type specified."""
@property
def readonly_property(self):
"""str: Properties should be documented in their getter method."""
return 'readonly_property'
@property
def readwrite_property(self):
""":obj:`list` of :obj:`str`: Properties with both a getter and setter
should only be documented in their getter method.
If the setter method contains notable behavior, it should be
mentioned here.
"""
return ['readwrite_property']
@readwrite_property.setter
def readwrite_property(self, value):
value
def example_method(self, param1, param2):
"""Class methods are similar to regular functions.
Note:
Do not include the `self` parameter in the ``Args`` section.
Args:
param1: The first parameter.
param2: The second parameter.
Returns:
True if successful, False otherwise.
"""
return True
def __special__(self):
"""By default special members with docstrings are not included.
Special members are any methods or attributes that start with and
end with a double underscore. Any special member with a docstring
will be included in the output, if
``napoleon_include_special_with_doc`` is set to True.
This behavior can be enabled by changing the following setting in
Sphinx's conf.py::
napoleon_include_special_with_doc = True
"""
pass
def __special_without_docstring__(self):
pass
def _private(self):
"""By default private members are not included.
Private members are any methods or attributes that start with an
underscore and are *not* special. By default they are not included
in the output.
This behavior can be changed such that private members *are* included
by changing the following setting in Sphinx's conf.py::
napoleon_include_private_with_doc = True
"""
pass
def _private_without_docstring(self):
pass
\end{minted}
\caption{Un exemple de documentation Napoleon}
\end{listing}
\begin{figure}[!ht]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/napoleon-module-level-docstring.png}}
\caption{\url{https://xkcd.com/353/}}
\end{figure}
Pour ceux que cela pourrait intéresser, il existe
\href{https://marketplace.visualstudio.com/items?itemName=njpwerner.autodocstring}{une
extension pour Codium}, comme nous le verrons juste après, qui permet de
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 \index{lint}}
Il existe plusieurs niveaux de \emph{linters}:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le premier niveau est assuré par \href{https://pypi.org/project/pycodestyle/}{pycodestyle}, qui analyse votre code à la recherche d'erreurs de convention de style.
\item
Le deuxième niveau est \href{https://pypi.org/project/pyflakes/}{pyflakes}.
Il s'agit d'un \emph{simple} \footnote{Ce n'est pas moi qui le dit, c'est la doc du
projet} programme qui recherchera différents types d'erreurs parmi votre code source.
\item
Le troisième niveau est
\href{https://pypi.org/project/flake8/}{Flake8}, qui regroupe les deux
premiers niveaux, en plus d'y ajouter flexibilité, extensions et une
analyse de complexité de McCabe.
\item
Le quatrième niveau \footnote{Oui, en Python, il n'y a que quatre
cercles à l'Enfer} est \href{https://pylint.org/}{PyLint}.
\end{enumerate}
PyLint est le meilleur ami de votre \emph{moi} futur, un peu comme quand vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire le lendemain: il rendra votre code soyeux et brillant, en posant des affirmations spécifiques.
A vous de les traiter en corrigeant le code ou en apposant un \emph{tag} indiquant que vous avez pris connaissance de la remarque, que vous en avez tenu compte, et que vous choisissez malgré tout de faire autrement.
\begin{graphic}{images/calvin/time-machine.jpg}
\caption{Calvin \& Hobbes se rendent dans le futur pour récupérer les devoirs que son moi-du-futur aura fait (et pour ne pas avoir à les faire lui-même).}
\end{graphic}
Pour vous donner une idée, voici ce que cela pourrait donner avec un
code pas très propre et qui ne sert à rien:
\begin{listing}[H]
\begin{minted}{python}
from datetime import datetime
"""On stocke la date du jour dans la variable ToD4y"""
ToD4y = datetime.today()
def print_today(ToD4y):
today = ToD4y
print(ToD4y)
def GetToday():
return ToD4y
if __name__ == "__main__":
t = Get_Today()
print(t)
\end{minted}
\caption{Un morceau de code qui ne sert à rien}
\end{listing}
\subsection{Flake8}
Avec Flake8, nous obtiendrons ceci:
\begin{listing}[!ht]
\begin{verbatim}
test.py:7:1: E302 expected 2 blank lines, found 1
test.py:8:5: F841 local variable 'today' is assigned to but never used
test.py:11:1: E302 expected 2 blank lines, found 1
test.py:16:8: E222 multiple spaces after operator
test.py:16:11: F821 undefined name 'Get_Today'
test.py:18:1: W391 blank line at end of file
\end{verbatim}
\end{listing}
Nous trouvons des erreurs:
\begin{itemize}
\item
de \textbf{conventions}: le nombre de lignes qui séparent deux
fonctions, le nombre d'espace après un opérateur, une ligne vide à la
fin du fichier, \ldots\hspace{0pt} Ces \emph{erreurs} n'en sont pas
vraiment, elles indiquent juste de potentiels problèmes de
communication si le code devait être lu ou compris par une autre
personne.
\item
de \textbf{définition}: une variable assignée mais pas utilisée ou une
lexème non trouvé. Cette dernière information indique clairement un
bug potentiel. Ne pas en tenir compte nuira sans doute à la santé de
votre code (et risque de vous réveiller à cinq heures du mat', quand
votre application se prendra méchamment les pieds dans le tapis).
\end{itemize}
\subsection{Pylint}
L'étape d'après consiste à invoquer pylint.
Lui, il est directement moins conciliant:
\begin{verbatim}
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
t = Get_Today()
^ (bad-whitespace)
test.py:18:0: C0305: Trailing newlines (trailing-newlines)
test.py:1:0: C0114: Missing module docstring (missing-module-docstring)
test.py:3:0: W0105: String statement has no effect (pointless-string-statement)
test.py:5:0: C0103: Constant name "ToD4y" doesn't conform to UPPER_CASE naming style (invalid-name)
test.py:7:16: W0621: Redefining name 'ToD4y' from outer scope (line 5) (redefined-outer-name)
test.py:7:0: C0103: Argument name "ToD4y" doesn't conform to snake_case naming style (invalid-name)
test.py:7:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:8:4: W0612: Unused variable 'today' (unused-variable)
test.py:11:0: C0103: Function name "GetToday" doesn't conform to snake_case naming style (invalid-name)
test.py:11:0: C0116: Missing function or method docstring (missing-function-docstring)
test.py:16:4: C0103: Constant name "t" doesn't conform to UPPER_CASE naming style (invalid-name)
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 d'aller en négatif).
En vrac, nous trouvons des problèmes liés:
\begin{itemize}
\item
au nommage (C0103) et à la mise en forme (C0305, C0326, W0105)
\item
à des variables non définies (E0602)
\item
de la documentation manquante (C0114, C0116)
\item
de la redéfinition de variables (W0621).
\end{itemize}
Pour reprendre la
\href{http://pylint.pycqa.org/en/latest/user_guide/message-control.html}{documentation},
chaque code possède sa signification:
\begin{itemize}
\item
\textbf{C}, pour toutes les vérifications liées aux conventions (nommage et mises en forme, que l'on a vues ci-dessus)
\item
\textbf{R}, pour des propositions de refactoring
\item
\textbf{W}, pour tout ce qui est en lien avec des avertissements
\item
\textbf{E} pour les erreurs ou des bugs probablement présents dans le code
\item
\textbf{F} pour les erreurs internes au fonctionnement de pylint, qui font que le traitement n'a pas pu aboutir.
\end{itemize}
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}.
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, \ldots
\item \textbf{Un projet entier}
\end{enumerate}
\subsubsection{Ignorer une ligne de code}
\subsubsection{Ignorer un bloc de code}
\subsubsection{Ignorer une catégorie globalement}
\subsection{Ruff}
Lui, c'est le petit chouchou à la mode.
\href{https://beta.ruff.rs/docs/}{ruff}
\begin{itemize}
\item
10-100x faster than existing linters
\item
Installable via pip
\item
Python 3.11 compatibility
\item
pyproject.toml support
\item
Built-in caching, to avoid re-analyzing unchanged files
\item
Autofix support, for automatic error correction (e.g., automatically remove unused imports)
\item
Near-parity with the built-in Flake8 rule set
\item
Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
\item
First-party editor integrations for VS Code and more
\item
Monorepo-friendly, with hierarchical and cascading configuration
\end{itemize}
\begin{listing}[H]
\begin{verbatim}
[tool.ruff]
# Enable Pyflakes `E` and `F` codes by default.
select = ["E", "F"]
ignore = []
# Allow autofix for all enabled rules (when `--fix`) is provided.
fixable = ["A", "B", "C", "D", "E", "F", "..."]
unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".hg",
".mypy_cache",
".nox",
".pants.d",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
per-file-ignores = {}
# Same as Black.
line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10.
target-version = "py310"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10
\end{verbatim}
\caption{Un morceau de code qui ne sert à rien}
\end{listing}
\section{Formatage de code}
Nous avons parlé ci-dessous de style de codage pour Python (PEP8) \index{PEP!PEP8}, de style de rédaction pour la documentation (PEP257) \index{PEP!PEP257}, d'un vérificateur pour nous indiquer quels morceaux de code doivent absolument être revus, \ldots
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
\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).
\begin{quote}
By using Black, you agree to cede control over minutiae of
hand-formatting. In return, Black gives you speed, determinism, and
freedom from pycodestyle nagging about formatting. You will save time
and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible.
Blackened code looks the same regardless of the project you're reading.
Formatting becomes transparent after a while and you can focus on the
content instead.
\end{quote}
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 "\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{PEP!PEP484} \index{PEP!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{Gestion des exceptions}
Certains bogues causent des erreurs de mal-fonctionnement sont dus à une mauvaise conception endormie, qui ne se présente que dans certains cas très spécifiques, entourés d'une contexte inhabituel \cite[p. 9]{data_design}.
Il est primordial de gérer correctement ses exceptions, et de faire en sorte que celles qui peuvent être anticipées le soient dès la phase de développement.
\section{Gestion des versions de l'interpréteur}
\begin{verbatim}
pyenv install 3.10
\end{verbatim}
\section{Matrice de compatibilité}
Une 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'objectif consiste à définir un tableau à deux dimensions, dans lequel nous trouverons la compatibilité entre notre application et une version de l'interpréteur.
\begin{center}
\begin{tabular}{|c|c|c|c|}
& py37 & py38 & py39 \\
\hline
lib2 & V & V & V \\
lib3 & X & V & V \\
lib4 & X & V & V
\end{tabular}
\end{center}
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{Glue de configuration}
Décrire le fichier setup.cfg.
\section{Makefile}
Pour gagner un peu de temps, n'hésitez pas à créer un fichier \texttt{Makefile} que vous placerez à la racine du projet.
L'exemple ci-dessous permettra, grâce à la commande \texttt{make\ coverage}, d'arriver au même résultat que ci-dessus:
\begin{listing}
\begin{verbatim}
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
\end{verbatim}
\caption{Un exemple de fichier Makefile}
\end{listing}
Pour la petite histoire, \texttt{make} peu sembler un peu désuet, mais
reste extrêmement efficace.
\section{Conclusions (et intégration continue)}
\subsection{setup.cfg}
→ Faire le lien avec les settings → Faire le lien avec les douze
facteurs → Construction du fichier setup.cfg
\begin{verbatim}
[flake8]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[pycodestyle]
max-line-length = 100
exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv
[mypy]
python_version = 3.8
check_untyped_defs = True
ignore_missing_imports = True
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[mypy.plugins.django-stubs]
django_settings_module = config.settings.test
[mypy-*.migrations.*]
# Django migrations should not produce any errors:
ignore_errors = True
[coverage:run]
include = khana/*
omit = *migrations*, *tests*
plugins =
django_coverage_plugin
\end{verbatim}
Mypy + black + pylint + flake8 + pyflakes + \ldots
\subsection{Libraries.io}
https://libraries.io/ (mais je ne sais pas encore quoi en faire)