Analyseur de Dépendances
Un requirements.txt dit qu’une équipe a l’intention d’utiliser cinq paquets. Les fichiers réellement écrits disent quels paquets sont réellement importés. La différence entre les deux est là où vivent le gaspillage et le risque : des pins inutilisés gonflent les installations à ce jour, et un numpy==1.26.0 épinglé peut s’asseoir deux versions mineures derrière le minimum de sécurité sans que personne ne le remarque jusqu’à ce qu’un bot scanne le manifeste. Ce projet construit le petit analyseur qui referme l’écart, analyse le manifeste, scanne les imports, et rapporte ce sur quoi les deux divergent, tout avec la bibliothèque standard.
Ceci suppose Python 101 plus une lecture confortable de pathlib et re. Rien du module Analyse de Données n’est nécessaire. C’est facultatif et non noté ; voir Projets du monde réel pour la liste complète et croissante.
🎯 Ce que tu vas faire
- Analyser
requirements.txten enregistrements(name, spec)et classifier chaque pin comme épinglé, borné, ou non épinglé. - Scanner une arborescence source
myapp/, classifier chaque import comme stdlib, tierce partie, ou local. - Croiser les deux : les dépendances déclarées mais jamais importées.
- Comparer les versions déclarées contre une ligne de base d’advisories locale de versions minimales.
- En faire une CLI avec des codes de sortie (
0= sain,1= deps inutilisées,2= violation de politique) pour qu’un build puisse agir sans analyser du texte.
Où exécuter ceci
En local avec uv est le chemin recommandé, tout le travail de l’outil est de parcourir ton répertoire, et un scanneur de répertoires fonctionne mieux comme CLI locale.
GitHub Codespaces est une alternative sans configuration : ouvre tout le dépôt du cours dans un Codespace gratuit (Node et Python sont déjà installés) et exécute les mêmes commandes depuis un terminal navigateur.
Google Colab, Kaggle Notebooks ou Binder fonctionnent pour chaque étape, le notebook dans examples/dependency-analyzer/notebook.fr.ipynb exécute le même analyseur sur un projet d’échantillon fourni. Le compromis honnête : les notebooks ne peuvent pas parcourir un référentiel arbitraire comme une CLI locale le peut.
Configuration
uv est un outil unique qui remplace toute la chaîne « install Python, puis pip, puis un outil d’environnement virtuel », et ce projet est pure bibliothèque standard.
macOS / Linux (terminal) :
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows (PowerShell) :
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
Ferme et rouvre ton terminal, puis confirme son installation :
uv --version
Ensuite, configure le projet :
uv init dependency-analyzer
cd dependency-analyzer
✅ Liste de vérification
- ✅
uv --versionaffiche un numéro de version. - ✅
dependency-analyzer/existe avec unpyproject.toml. - ✅
python -c "import re, sys, pathlib"réussit, aucun paquet tiers.
Étape 1 : Analyser requirements.txt
Tout commence du manifeste : une dépendance par ligne, parfois épinglée (==2.31.0), parfois bornée (>=2.0, ~=3.0), parfois lâche (aucune spec du tout). Analyser signifie extraire (name, spec) et classifier la spec, car « ce pin est-il vieux ? » veut dire des choses différentes pour une version verrouillée que pour un intervalle ouvert.
1.1 Écris l’analyseur
👟 Indice de départ : Sépare les commentaires en ligne et les lignes d’options, prends le nom du paquet avant le premier espace, et classifie la spec avec deux petites regex :
# parse_req.py
import re
def parse_requirements(path: str) -> list[dict]:
deps: list[dict] = []
with open(path) as f:
for line in f:
line = line.split("#", 1)[0].strip()
if not line or line.startswith(("-", ".", "[")):
continue
m = re.match(r"^([A-Za-z0-9_.\-]+)\s*(.*)$", line)
if not m:
continue
deps.append({"name": m.group(1).lower(), "spec": m.group(2).strip()})
return deps
def classify_spec(spec: str) -> str:
if re.fullmatch(r"==[\d.]+", spec):
return "pinned"
return "ranged" if spec else "unpinned"
if __name__ == "__main__":
for d in parse_requirements("myapp/requirements.txt"):
print(f"{d['name']:<12} {d['spec'] or '<any>':<16} {classify_spec(d['spec'])}")
line.split("#", 1)[0] retire les commentaires en ligne (requests==2.31.0 # prod) avant tout le reste ; la garde line.startswith(...) saute les lignes de machinerie comme --index-url et . (une dépendance de chemin local). La regex classify_spec est délibérément stricte sur ce qui compte comme un pin : ==2.31.0 est verrouillé, tandis que >=2.0 et ~=3.0 sont des intervalles qui dérivent.
🎯 Résultat attendu :
requests ==2.31.0 pinned
pandas >=2.0 ranged
numpy ==1.26.0 pinned
flask ~=3.0 ranged
click >=8.0 ranged
🩹 Si ça ne marche pas : Si les noms sortent capitalisés, le .lower() sur m.group(1) manque, les noms de paquets sont insensibles à la casse sur PyPI mais les chemins de fichiers ne le sont pas, donc normalise en minuscules par avance. Si --index-url https://... finit comme une « dépendance », la garde - ne se déclenche qu’avant que .strip() l’ait découpée, vérifie l’ordre des gardes : split → strip → sauter les vides → sauter les lignes qui ressemblent à des options.
1.2 Vérifie l’analyseur
✅ Liste de vérification
- ✅ Un commentaire
#sur sa propre ligne et en ligne après un pin sont tous deux ignorés. - ✅ Le style
pip freezepackage==1.2.3et le PEP 440package>=1.2,<2produisent tous deux des paires(name, spec). - ✅ Les lignes jamais vues dans
requirements.txt, vides, options,-r other.txt, sont sautées sans crasher.
🤔 Question(s) socratique(s)
- Épinglé vs borné vs non épinglé est une classification d’un bit. Un
~=3.0(version compatible) et un>=20,<21(borne supérieure) pin différemment mais disent tous deux « ranged ». Que devrait ajouter un analyseur de spec plus riche pour distinguer « dérive bornée » de « dérive ouverte », et lequel des deux un scan de sécurité devrait-il traiter comme plus risqué ? -e .(installations locales éditables) et-r base.txt(inclut un autre fichier) commencent tous deux par-et sont sautés. Qu’est-ce qui est faux à les ranger sous « options », que signifient ces deux-là réellement pour l’ensemble de dépendances ?
Étape 2 : Scanner les imports
Le manifeste est un côté de la vérité ; le code est l’autre. Scanner signifie parcourir chaque .py sous ta racine de projet, tirer le nom de module de chaque import x / from x import y, et classifier chaque nom comme stdlib (vérifie contre sys.stdlib_module_names), le tien (un préfixe de projet), ou tierce partie. Le troisième groupe est celui qui est comparé au manifeste.
2.1 Écris le scanneur
👟 Indice de départ : Une regex ancrée à la ligne pour les instructions d’import, Path.rglob("*.py") pour le parcours, et sys.stdlib_module_names pour la classification, tout en stdlib :
# scan.py
import re
import sys
from pathlib import Path
IMPORT_RE = re.compile(r"^\s*(?:import|from)\s+([\w.]+)", re.M)
STDLIB = set(sys.stdlib_module_names)
def scan_directory(root: str) -> set[str]:
imports: set[str] = set()
for path in Path(root).rglob("*.py"):
imports |= {m for m in IMPORT_RE.findall(path.read_text())}
return {name.split(".")[0] for name in imports}
def classify(imports: set[str], project: str) -> tuple[set[str], set[str], set[str]]:
stdl, third, local = set(), set(), set()
for name in imports:
if name in STDLIB:
stdl.add(name)
elif name == project or name.startswith(project + "."):
local.add(name)
else:
third.add(name)
return stdl, third, local
if __name__ == "__main__":
Path("myapp").mkdir(exist_ok=True)
Path("myapp/app.py").write_text(
"import os\nimport sys\nimport requests\nimport pandas as pd\n"
"from myapp.utils import normalize\n")
Path("myapp/utils.py").write_text(
"import datetime\nimport numpy as np\n"
"def normalize(value):\n return value\n")
imports = scan_directory("myapp")
stdl, third, local = classify(imports, project="myapp")
print("stdlib:", sorted(stdl))
print("third-party:", sorted(third))
print("local:", sorted(local))
Le scan normalise from pandas import DataFrame et import pandas as pd vers le même nom de premier niveau pandas, name.split(".")[0] réduit aussi myapp.utils à myapp, donc chaque import s’effondre vers le mot unique que le manifeste déclarerait. sys.stdlib_module_names est tout l’intérêt de cette génération de Python : un ensemble curé de noms stdlib, aucune liste codée en dur à maintenir. Les deux fichiers de démo existent pour être scannés, pas exécutés, app.py utilise pandas qui n’est pas installé ici, et c’est exactement pourquoi tu n’exécutes pas le code que tu analyses.
🎯 Résultat attendu :
stdlib: ['datetime', 'os', 'sys']
third-party: ['numpy', 'pandas', 'requests']
local: ['myapp']
🩹 Si ça ne marche pas : Si myapp apparaît dans le groupe stdlib, sys.stdlib_module_names n’est pas présent (Python < 3.10), tout l’ensemble STDLIB est alors vide, donc tout retombe en tierce partie ; exécute sur 3.10+. Si les imports au milieu d’un fichier sont ratés, IMPORT_RE utilise ^ avec le drapeau re.M, retire re.M et seuls les import en début de ligne correspondent, ce qui sautera silencieusement les imports indentés à l’intérieur des fonctions (du Python valide, et la regex ne peut pas les distinguer).
2.2 Vérifie le scan
✅ Liste de vérification
- ✅ Les noms de bibliothèque standard (
os,sys,datetime) atterrissent en stdlib, pas en tierce partie, la classification utilisesys.stdlib_module_names, pas une devinette de machine à écrire. - ✅
import pandas as pd,from myapp.utils import normalize, etimport requestss’effondrent tous verspandas/myapp/requests. - ✅ Un répertoire sans fichier
.pyproduit un ensemble d’imports vide, pas un crash.
🤔 Question(s) socratique(s)
- Le scanneur est basé sur le texte : il lit des jetons
import, pas du code.import numpy as np # dans un commentaireserait attrapé, etif False: import numpyaussi. Qu’un scanneur basé sur AST (le moduleast) ajoute-t-il au-delà de la regex, et que ne connaît-il toujours pas qu’un profil d’exécution (import foopuisfoo()au moment de l’exécution) le ferait ? - Les imports relatifs (
from . import x,from ..y import z) disparaissent silencieusement de ce scanneur. Pourquoi.échoue la regex ancrée\w, et rater un import relatif est-il un échec sûr pour un rapport « dépendance inutilisée » ou un échec dangereux ?
Étape 3 : Trouver les dépendances inutilisées
Voici maintenant le paiement d’avoir les deux côtés : déclaré (depuis requirements.txt) moins importé (ce que le code tire réellement). Tout ce qui est déclaré-mais-pas-importé est soit du poids mort à couper soit un signe que le scan manque quelque chose, les deux valent un regard humain. La vérification est une différence d’ensembles ; l’honnêteté est dans le fait d’admettre que la différence d’ensembles n’est aussi bonne que le scanneur.
3.1 Écris find_unused
👟 Indice de départ : Une fonction, une soustraction d’ensemble, une liste triée à la sortie, la valeur n’est pas l’arithmétique, c’est que tu as deux ensembles fiables à soustraire :
# unused.py
def find_unused(declared: set[str], imported: set[str]) -> list[str]:
return sorted(declared - imported)
3.2 Lance-le sur le projet
# step3.py
from parse_req import parse_requirements
from scan import scan_directory
from unused import find_unused
declared = {d["name"] for d in parse_requirements("myapp/requirements.txt")}
imported = scan_directory("myapp")
for name in find_unused(declared, imported):
print(f"unused: {name}")
Trois paquets importés (requests, pandas, numpy) correspondent à trois déclarés ; flask et click sont déclarés mais jamais importés. La direction inverse, importé mais pas déclaré, est tout aussi juteuse et un changement d’une ligne (imported - declared), mais c’est un bug différent : ton code ne s’installera pas dans un environnement frais du tout. La portée décidée ici est « déclaré mais inutilisé », car c’est la branche sur laquelle tu peux agir immédiatement (supprime les lignes) et parce que le travail frère d’environnement frais est souvent le travail d’un outil séparé.
🎯 Résultat attendu :
unused: click
unused: flask
🩹 Si ça ne marche pas : Si pandas apparaît comme inutilisé, le classifieur l’a envoyé vers le groupe local (le préfixe de projet a-t-il correspondu à pandas. ?), puis il n’atterrit jamais dans imported pour la soustraction. Vérifie l’ordre des elif de classify. Si tout est inutilisé, scan_directory a parcouru la mauvaise racine, la démo scanne myapp/, donc confirme que le chemin requirements.txt et le --dir sont le même arbre.
3.3 Vérifie la liste d’inutilisés
✅ Liste de vérification
- ✅ L’ensemble déclaré est
{requests, pandas, numpy, flask, click}; l’ensemble importé est{os, sys, datetime, requests, pandas, numpy, myapp}; la différence est exactement{click, flask}. - ✅ La sortie des inutilisés est alphabétisée (triée), donc les tests peuvent dépendre de l’ordre.
- ✅ Retirer
flask~=3.0etclick>=8.0derequirements.txtvide la liste d’inutilisés, l’outil trouve les pins morts, il ne les imagine pas.
🤔 Question(s) socratique(s)
- Collision de nom régionale : tu déclares
requests(le paquet PyPI) mais tu as aussi un module localrequests/, la soustraction d’ensembles voit une dépendance utilisée et reste silencieuse. Qu’un outil doit-il ajouter (scénario : vérifie comment un nom est importé, ex.from requests import Sessionvsimport requests.utilsqui choisit un fichier local) avant de pouvoir appeler cette colonne « utilisé vérifié » ? clicketflasksont « inutilisés » selon le scan, maisflaskcharge souvent un autre plugin déclaré par point d’entrée, pas par import. Qu’est-ce que cela dit d’un analyseur qui ne voit que les lignesimport, « inutilisé » est-il un verdict ou une alerte ?
Étape 4 : Vérifier les versions contre la ligne de base d’advisories
Inutilisé est du gaspillage ; hors politique est du risque. Cette étape compare chaque spec déclarée contre un registre d’advisories local, un dict de versions minimales acceptables. Il se tient en lieu et place des tuyaux du monde réel (pip-audit, OSV, métadonnées PyPI), qui ont besoin d’appels réseau ; même forme, honnête sur la substitution. Un ==1.26.0 épinglé sous le plancher >=1.30 reçoit la ligne rouge.
4.1 Écris le vérificateur de versions
👟 Indice de départ : Extrais une version numérique de chaque côté de spec avec une regex lâche, compare comme tuples d’entiers, et décris le résultat par dépendance :
# health.py
import re
ADVISORY = {
"requests": ">=2.28",
"numpy": ">=1.30",
"flask": ">=2.2",
"pandas": ">=1.5",
}
def version_tuple(spec_part: str) -> tuple[int, ...]:
m = re.search(r"\d+(?:\.\d+)*", spec_part)
return tuple(int(p) for p in m.group(0).split(".")) if m else (0,)
def check_advisories(name: str, spec: str) -> str:
rule = ADVISORY.get(name)
if not rule:
return "not in advisory registry"
mine = version_tuple(spec) if spec else (0,)
minimum = version_tuple(rule)
state = "ok" if mine >= minimum else "BELOW ADVISORY MINIMUM"
have = ".".join(map(str, mine))
return f"{state} (have {have}, min {'.'.join(map(str, minimum))})"
# step4.py
from parse_req import parse_requirements
from health import check_advisories
for d in sorted(parse_requirements("myapp/requirements.txt"), key=lambda d: d["name"]):
print(f"{d['name']:<12} {d['spec'] or '<any>':<16} {check_advisories(d['name'], d['spec'])}")
version_tuple est toute la comparaison en huit lignes : il attrape la première course major.minor(.patch) de n’importe quelle chaîne de spec, donc ==2.31.0, ~=3.0, et >=2.28 deviennent tous des tuples d’entiers valant la comparaison. La comparaison de tuples d’entiers est l’ordre de versions intégré de Python : (2, 31, 0) >= (2, 28) vaut True, (1, 26, 0) >= (1, 30) vaut False, aucun piège de tri de chaînes. Une dep déclarée non épinglée (click sans spec) reçoit (0, ...), traitée comme « pourrait être n’importe quoi », donc la lettre du registre la décide.
🎯 Résultat attendu :
click >=8.0 not in advisory registry
flask ~=3.0 ok (have 3.0, min 2.2)
numpy ==1.26.0 BELOW ADVISORY MINIMUM (have 1.26.0, min 1.30)
pandas >=2.0 ok (have 2.0, min 1.5)
requests ==2.31.0 ok (have 2.31.0, min 2.28)
🩹 Si ça ne marche pas : Si version_tuple("~=3.0") retourne (0,), la regex cherche des chiffres ancrés (^\d+) au lieu d’une recherche, ~ précède le 3. Si click montre ok au lieu de not in advisory registry, ADVISORY.get(name) prend le défaut, ce qui signifie qu’une clé style numpy n’est pas click, les clés de chaîne sont exactes ; les manques de registre sont le résultat conçu, pas un repli.
4.2 Vérifie la vérification d’advisories
✅ Liste de vérification
- ✅ Un paquet sous son minimum (
numpy) est signalé ; un à/au-dessus (requests,flask,pandas) est « ok ». - ✅ Un paquet sans entrée de registre (
click) est rapporté comme non revu, pas silencieusement absent. - ✅ Pas de trucs de chaîne ad hoc :
~=3.1et>=3.1comparent égaux comme tuples, et2.28≠2.28.1, la longueur du tuple fait partie de l’ordre.
🤔 Question(s) socratique(s)
version_tuple("~=3.0")retourne(3, 0)et le compare comme au moins 3.0. En PEP 440,~=3.0signifie vraiment>=3.0, <4, « version compatible ». Qu’ignorer la borne supérieure fait prétendre à ton analyseur qu’il ne peut pas réellement promettre ?- Le registre est un dict local. Dans un vrai projet, il viendrait d’un flux interrogeable (JSON PyPI, OSV). Que la forme de la comparaison change quand la source de vérité est une API vivante, et qu’est-ce qui commence à échouer quand il n’y a pas de réseau dans la CI ?
Étape 5 : La CLI et le code de sortie
Le travail de l’analyseur est terminé quand un script de build peut traiter la réponse comme un verdict, pas un vidage de texte. La CLI prend --dir, compose analyse → scan → inutilisé → advisory, affiche trois lignes de résumé, et retourne 0 (sain), 1 (deps inutilisées), ou 2 (violation d’advisory), donc la CI peut échouer sur $? sans lire ton rapport du tout.
5.1 Écris analyze.py
👟 Indice de départ : argparse pour --dir, réutilise chaque fonction des étapes précédentes, et règle sys.exit depuis les deux groupes d’échec :
# analyze.py
import argparse
import sys
from pathlib import Path
from health import check_advisories
from parse_req import parse_requirements
from scan import scan_directory
from unused import find_unused
def main() -> None:
parser = argparse.ArgumentParser(description="Analyze a project's Python dependencies.")
parser.add_argument("--dir", default=".")
args = parser.parse_args()
root = Path(args.dir)
req = parse_requirements(root / "requirements.txt")
imported = scan_directory(str(root))
declared = {d["name"] for d in req}
unused = find_unused(declared, imported)
policy_budget = 0
warnings = []
for d in sorted(req, key=lambda d: d["name"]):
report = check_advisories(d["name"], d["spec"])
if "BELOW" in report:
policy_budget = 2
warnings.append(f"{d['name']} {d['spec']}: {report}")
print(f"declared: {len(req)} used: {len(declared & imported)} unused: {len(unused)}")
for name in unused:
print(f"unused: {name}")
for w in warnings:
print(f"advisory: {w}")
print("result:", "FAIL" if (unused or policy_budget) else "OK")
sys.exit(1 if unused else policy_budget)
if __name__ == "__main__":
main()
uv run python analyze.py --dir myapp
La politique de code de sortie est un choix, écrit là où un réviseur peut le voir : inutilisé gagne (1) sur advisory (2) ; propre gagne (0). Composer tout le pipeline depuis des fonctions que tu possèdes signifie qu’un futur réglage « bloquer sur inutilisé » est un changement .py d’une ligne, pas une réécriture.
🎯 Résultat attendu :
declared: 5 used: 3 unused: 2
unused: click
unused: flask
advisory: numpy ==1.26.0: BELOW ADVISORY MINIMUM (have 1.26.0, min 1.30)
result: FAIL
Relance la commande shell et echo $? affiche 1.
🩹 Si ça ne marche pas : Si FileNotFoundError se déclenche pour requirements.txt, --dir pointe vers un répertoire qui n’en contient pas un, la CLI attend ton manifeste à l’intérieur de la racine scannée, correspondant à ce que l’analyseur vérifie. Si exit code: 0 s’affiche malgré des paquets inutilisés, sys.exit(1 if unused else policy_budget) manque, la ligne print("result: ...") est véridique, le code de sortie est le contrat.
5.2 Vérifie la CLI
✅ Liste de vérification
- ✅
uv run python --dir myappaffiche le résumé ci-dessus etecho $?est1. - ✅ Supprimer
click/flaskderequirements.txttransforme l’exécution enresult: OK, sortie0. - ✅ Élever le pin de
numpyà==1.30.0efface l’advisory et gardeunusedà zéro, le code de sortie dérive à la fois des données et lit les mêmes fichiers que tu survoles.
🤔 Question(s) socratique(s)
- Les codes de sortie 1 et 2 s’effondrent quand les deux conditions tiennent (résolu,
1gagne). Si un build veut distinguer « code mort, bloque » de « release de sécurité en attente, préviens », les codes doivent se composer (ex. 1 = inutilisé, 2 = advisory, 3 = les deux). Qu’est-ce qui change danssys.exit(...)pour faire 3 = les deux une one-liner, et la CI s’en soucie-t-elle ? declared & importedcompte un paquet utilisé une fois comme utilisé ; il n’y a pas de signal d’intensité « importé onze fois dans neuf fichiers ». Qu’une dimension fréquence ajouterait-elle au tri du rapport, et qui est le lecteur du rapport qui l’utiliserait réellement ?
⚠️ Pièges courants
- Correspondance d’import par sous-chaîne. Correspondre
import oscontreos.pathouosx-toolsa besoin de frontières de mots, la regex de jetons([\w.]+)juste aprèsimport|fromte donne déjà le nom de premier niveau, donc ne teste pas les noms de modules avecin. - Faire confiance à un seul côté. Déclaré-sans-importé = inutilisé ; importé-sans-déclaré = fresh installs cassés. Un analyseur qui ne répond qu’à une direction écrit un demi-rapport. (Inverse la soustraction et le deuxième bug est gratuit.)
- Bruit de manifeste.
--index-url,-r,., les lignes#commentne sont pas des dépendances. Un analyseur qui frappe une « dépendance » appelée--index-urlcorrompt chaque nombre en aval. - Les tuples de versions ne sont pas des chaînes.
"9.0" < "10.0"estFalselexicalement mais(9,0) < (10,0)estTruenumériquement, compare toujours via des tuples d’entiers dans ce projet. - Avale les préfixes de spec.
version_tupleattrapant29depuis>=29,<30rate la casquette<30et l’écart de mapping des noms (python-dateutilimporté commedateutil) signifie qu’un analyseur « inoffensif » bénit silencieusement un paquet réellement utilisé comme inutilisé. Le rapport se lit comme un scan, juge comme un humain.
Ce que tu viens de construire
Un analyseur de dépendances sans dépendances de lui-même : analyseur de manifeste, scanneur d’imports, différenciateur par différence d’ensembles, vérificateur de versions d’advisories, et une porte de code de sortie, cinq fichiers, un verbe CLI, et un rapport que trois lignes résument. La leçon transférable est la triangulation : un manifeste et un scan de code racontent chacun une histoire partielle, et la valeur de l’outil est précisément les endroits où les deux ne sont pas d’accord, des pins inutilisés à tailler, des versions hors politique à élever, et (avec la soustraction inversée) des dépendances que tu as oublié de déclarer du tout.
Exécute une version plus complète sans aucune configuration locale
examples/dependency-analyzer/ dans le dépôt du cours contient les scripts complets, le projet d’échantillon myapp/, et un registre d’advisories d’échantillon. Ou ouvre tout le dépôt dans un GitHub Codespaces.
Où aller à partir d’ici
- Scanne la direction inverse (
imported - declared) comme deuxième colonne de rapport : « déclaré nulle part mais importé partout = les fresh installs plantent », gratteur de bugs gratuit maintenant que la machinerie existe. - Publie le résumé en JSON (
--json), pour qu’un tableau de bord ou un bot de PR puisse rendre les verdicts sans re-analyser ton rapport humain. - Ajoute un scanneur basé sur
astcomme deuxième source d’imports, et signale les paquets où les scanneurs regex et AST sont en désaccord, trie les endroits où vivent les imports douteux. - Colle l’écart de mapping des noms avec un dict d’alias (
python-dateutil→dateutil,beautifulsoup4→bs4) pour que la différence d’ensembles cesse de raté sur la moitié des noms de PyPI.
Partage ton projet avec la classe
Tu as construit quelque chose dont tu es fier ? examples/student-projects/ est une galerie de projets soumis par d’autres élèves, et son README a un tutoriel complet et adapté aux débutants pour ajouter le tien via une pull request, même si tu n’as jamais utilisé git avant : forker le dépôt, créer une branche, commiter tes fichiers, et ouvrir la PR, une étape à la fois. Aucune expérience préalable avec git n’est supposée.
Bienvenue dans l’écriture de Python en dehors du navigateur. 🎓
Terminez chaque étape puis marquez ce projet comme terminé pour gagner ses points.