Comment faire du code Python méga robuste ?
Jonathan “Rambo” Gaffiot et Julien “Colonel” Lenormand
Un peu d’entretien sur un projet legacy, juste quelques features à ajouter et quelques bugs à régler…
ruff format .
from pandas import *
import numpy as np; import scipy as sp
a_dict = { "aaaaaaaaaa" : "aaaaaaaaaa", "bbbbbbbbbbb": "bbbbbbbbbbb",
"""yes triple quotes are legal here""": """guaranteed"""}
def a_function(df: DataFrame, very_long_argument=None, another_one="spam") -> dict[str, list[float | None]]:
'''Never forget the docstring!'''
if df.isnotempty() and len(very_long_argument) > 42:
print("I can use as many spaces that I want!!!!")
return {"pi": [3.1415926535]}
a_function(a_dict, another_one="bites the dust")
from pandas import *
import numpy as np
import scipy as sp
a_dict = {
"aaaaaaaaaa": "aaaaaaaaaa",
"bbbbbbbbbbb": "bbbbbbbbbbb",
"""yes triple quotes are legal here""": """guaranteed""",
}
def a_function(
df: DataFrame, very_long_argument=None, another_one="spam"
) -> dict[str, list[float | None]]:
"""Never forget the docstring!"""
if df.isnotempty() and len(very_long_argument) > 42:
print("I can use as many spaces that I want!!!!")
return {"pi": [3.1415926535]}
a_function(a_dict, another_one="bites the dust")
ruff check --statistics .
(sinon ça tient pas sur le
slide)2 ANN001 [ ] missing-type-function-argument
1 T201 [*] print
1 ARG001 [ ] unused-function-argument
1 D100 [ ] undocumented-public-module
1 D400 [ ] ends-in-period
1 F403 [ ] undefined-local-with-import-star
1 F405 [ ] undefined-local-with-import-star-usage
1 PLR2004 [ ] magic-value-comparison
liste hétérogène :
events = [EventA(), None, 42, "spam", [{}]]
parsing :
MyClass(**json.loads(request.text))
duck typing
modification à la volée
liste de type
Annotations : d’abord une documentation pour les devs, et ensuite
pour l’IDE et mypy
mypy
déduit les types quand il peut, et vérifie que
les types sont compatibles
my-script.py:14: error: "Series[Any]" not callable [operator]
my-script.py:19: error: Argument 1 to "a_function" has incompatible type "dict[str, str]"; expected "DataFrame" [arg-type]
Found 2 errors in 1 file (checked 1 source file)
.pre-commit-config.yaml
:
Si activé (pre-commit install
) se lance automatiquement
à chaque commit
rambo:~/awesome-code(uber-feature *$% u)$ git commit -a -m "Adrienne!"
Ruff as Formatter........................................................Passed
Ruff as Linter...........................................................Passed
MyPy.....................................................................Passed
Fichier dodo.py
:
def task_fmt() -> dict:
"""Format the code."""
return {"actions": ["uv run ruff format ."]}
def task_lint() -> dict:
"""Lint the code."""
return {"actions": ["uv run ruff check ."]}
def task_type() -> dict:
"""Check the types."""
return {"actions": ["uv run mypy ."]}
def task_pycheck() -> dict:
"""Run all the Python quality tools."""
return {
"actions": None,
"verbosity": 1,
"task_dep": ["fmt", "lint", "type"],
}
Permet de lancer des commandes :
rambo:~/awesome-code(uber-feature *$% u)$ doit list
fmt Format the code.
lint Lint the code.
pycheck Run all the Python quality tools.
type Check the types.
rambo:~/awesome-code(uber-feature *$% u)$ doit pycheck
. fmt
. lint
. type
# Pseudo code
build: # image finale pour livraison
image: ghcr.io/astral-sh/uv:python3.12-bookworm-slim # uv déjà installé
script:
- uv sync --frozen # Installation des dépendances
artifacts:
image: [my-app:latest, my-app:$VERSION] # Sauvegarde de l'image
test: # étape de validation
from: build # on repart de l'image de prod
script:
- uv sync --frozen --dev # on installe les dépendances de dev
- uv run ruff format . # on teste le formattage
- uv run ruff check . # on passe l'analyse statique
- uv run mypy . # on vérifie les types
- uv run pytest # on passe les tests
- mkdir report # on sauvegarde les résultats pour analyse éventuelle
- mv .pytest_cache/report.html report/pytest_report.html
artifacts:
paths:
- report
my-lib.py
:
test_my-lib.py
:
import random
import pytest
from my-lib import add
@pytest.fixture()
def setup_and_tear_down():
print('This is run before each test')
yield
print('This is run after each test')
def test_add(setup_and_tear_down):
assert add(1, 2) == 3
assert add(1, -2) == -1
assert add("1", "2") == "12"
assert add(0.1, 0.2) == pytest.approx(0.3) # déjà essayé 0.1+0.2==0.3 au prompt ?
a, b = random.random(), random.random()
assert add(a, b) == add(b, a)
with pytest.raises(TypeError):
add(1, "2")
Et y’a plus qu’à lancer pytest
.
# try/finally
db = SqliteDatabase('test.db')
try:
db.connect()
do_stuff(db)
finally:
db.close()
# context manager
with open("my_file.txt") as file:
print(file.read())
# module contextlib
from contextlib import closing
with closing(urlopen('https://www.python.org')) as page:
print([line for line in page])
# module atexit
import atexit
file = open("output.log")
atexit.register(file.close)
Seul mécanisme d’erreur en Python, du crash à la sortie de boucle (si)
Toutes les exceptions standards dérivent de
BaseException
:
Mais on n’utilise jamais
BaseException(Group)
, on attrape au plus
Exception
On raise ses propres exceptions, pour pouvoir trier :
class AppError(Exception):
"""The app top error"""
class AppConfigError(AppError):
"""Error reading and validating the configuration"""
class AppTimeoutError(AppError, TimeoutError): # bon usage de l'héritage multiple
"""Timeout of the app."""
raise AppConfigError("Qui a ENCORE oublié le fichier de conf ?")
Avec des except
multiples, du plus sélectif au moins
sélectif :
try:
success = func()
except TimeoutError: # tous les timeout, de la lib de connexion comme de mon code
retry()
except AppError as err: # autre erreur de mon code
if err.code >= 400: # on peut se servir du contenu de l'erreur si y'en a
log.warn("quand serveur fâché, lui toujours faire ainsi")
retry()
else:
log.error("j'ai laisser trainer un truc, rhoo spa graaaave...")
backup()
except Exception: # voiture balai, seulement si nécessaire (aucun tri)
log.exception("oupsi")
reboot()
logging
, souvent indirectement
Très puissant, mais lourd à configurer
Fait cascader les logs de tous les fichiers, toutes les libs vers le logger configuré par l’utilisateur
import toml
import logging
import logging.config # oui c'est obligatoire, et non je sais pas pourquoi
from pathlib import Path
import mylib
log = logging.getLogger(__name__)
def main():
if not Path("log-config.toml").exists():
# basicConfig pour de petits trucs
logging.basicConfig(filename='myapp.log', level=logging.INFO)
else:
# sinon il faut définir formatter, handler, logger...
# => on peut mettre la conf du logger dans un fichier une fois pour toute
logging.config.dictConfig(toml.load("log-config.toml"))
log.info('Started')
mylib.do_something()
log.info('Finished')
if __name__ == '__main__':
main()
logging.handlers.RotatingFileHandler
rambo:~/awesome-code(doc *$% u)$ uv add --dev sphinx
rambo:~/awesome-code(doc *$% u)$ mkdir doc && cd doc
rambo:~/awesome-code(doc *$% u)$ uv run sphinx-quickstart .
Bienvenue dans le kit de démarrage rapide de Sphinx 8.0.2.
Veuillez saisir des valeurs pour les paramètres suivants.
Chemin racine sélectionné : .
Vous avez deux options pour l'emplacement du répertoire de construction de la sortie de Sphinx.
> Séparer les répertoires source et de sortie (y/n) [n]: y
Le nom du projet apparaîtra à plusieurs endroits dans la documentation construite.
> Nom du projet: gros-missile-secret
> Nom(s) de(s) l'auteur(s): Rambo
> Version du projet []: 6.6.6
Fichier en cours de création doc/source/conf.py.
Fichier en cours de création doc/source/index.rst.
Fichier en cours de création doc/Makefile.
Fichier en cours de création doc/make.bat.
Terminé : la structure initiale a été créée.
Vous devez maintenant compléter votre fichier principal /home/rambo/awesome-code/doc/source/index.rst et créer d'autres fichiers sources de documentation. Utilisez le Makefile pour construire la documentation comme ceci :
make builder
où « builder » est l'un des constructeurs disponibles, tel que html, latex, ou linkcheck.
rambo:~/awesome-code(doc *$% u)$ uv run sphinx-apidoc -o source/apidoc ..
rambo:~/awesome-code(doc *$% u)$ tree
.
├── build
├── make.bat
├── Makefile
└── source
├── apidoc
│ ├── main.rst
│ ├── modules.rst
│ └── src.rst
├── conf.py
├── index.rst
├── _static
└── _templates
rambo:~/awesome-code(doc *$% u)$ make html
Sphinx v8.0.2 en cours d'exécution
chargement des traductions [en]... fait
[...]
La compilation a réussi.
Les pages HTML sont dans build/html.
rambo:~/awesome-code(doc *$% u)$ firefox build/html/index.html
Rappel : “Vous devez maintenant compléter votre fichier principal
index.rst
et créer d’autres fichiers sources de
documentation.”
le code le moins cher est celui qui n’a pas besoin d’être écrit
le second code le moins cher est celui que quelqu’un d’autre paye pour écrire et que vous pouvez juste réutiliser librement
requests
,
numpy
/pandas
, flask
,
pytest
…uv build && uv publish
# Image Debian/Python avec uv
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim as build
WORKDIR /app
COPY . . # copie le code dans l'image
RUN uv sync --frozen --no-dev # installe les dépendances de prod
# Image Debian/Python sans uv
FROM python:3.12-slim as prod
WORKDIR /app
# Copie depuis le stage de build des fichiers nécessaires
COPY --from=build /app/main.py /app
COPY --from=build /app/src /app/src
COPY --from=build /app/log_config.toml /app/log_config.toml
COPY --from=build /app/.venv /app/.venv
RUN mkdir log
# Lance le main en acceptant les commandes utilisateurs :
# docker run my-app:latest --arg1 value --flag
ENTRYPOINT ["/app/.venv/bin/python", "/app/main.py"]
Et plus qu’à docker build -t my-app:latest .
Application avec une fonction de sécurité : possible (langage memory safe, thread safe) mais pas le langage de prédilection
Faire attention aux bases et déléguer :
Mise à jour, mise à jour, mise à jour, MISE À JOUR
L’exploitation de faille peut aller très vite
Oui mais si la mise à jour fait tout péter ?
Algos à connaitre, en bien ou en mal (en cas de doute : ANSSI)
Mathématiquement aucune attaque connue, par contre très dur à implémenter sans bug ni faille
~ 10^47 J
1eV
(=énergie
d’un photon) par test : > 10^58 J
perplexity.ai
pour la recherche Web + génération de
bouts de code pour les autresCD > CI > Agile && CD > DevOps && CD in facile_de_dire_si_on_en_fait