Compétence Couverture de types Pyrefly
Prérequis
- Le fichier doit se trouver dans un projet avec un
pyrefly.toml. pyrefly,lintrunneret le test runner du projet doivent être sur PATH. S'il en manque un, arrête-toi et demande si un environnement conda doit être activé — ne les installe ni ne les remplace pas (selon le CLAUDE.md du repo).
Étape 1 : Supprimer les suppressions de vérification de types au niveau du fichier
Supprime l'un de ces éléments du haut du fichier (pyrefly honore # mypy: ignore-errors pour la compatibilité mypy, donc celui-ci doit aussi disparaître) :
# pyre-ignore-all-errors
# pyre-ignore-all-errors[16,21,53,56]
# @lint-ignore-every PYRELINT
# mypy: ignore-errors
Étape 2 : Ajouter une entrée de sous-config à pyrefly.toml
[[sub-config]]
matches = "path/to/directory/**"
[sub-config.errors]
implicit-import = false
implicit-any = true
bad-param-name-override = false
unannotated-return = true
unannotated-parameter = true
IMPORTANT : Définir une clé d'erreur dans [sub-config.errors] ne remplace que cette clé par rapport au parent — mais activer unannotated-return / unannotated-parameter / implicit-any va resurfacer des erreurs qui étaient auparavant masquées au niveau du fichier. Si tu vois des erreurs sans rapport (ex. bad-param-name-override) inonder la sortie, reproduis le paramètre du parent config pour cette clé dans la sous-config pour les silencer.
Étape 3 : Exécuter pyrefly
pyrefly check <FILENAME>
Objectif : résoudre tous les erreurs unannotated-return, unannotated-parameter et implicit-any en ajoutant des annotations — voir l'échelle à l'Étape 4. Ces trois catégories cibles sont toujours résolubles ; ne jamais les supprimer avec # pyrefly: ignore. La seule exception est @compatibility(is_backward_compatible=True) (Étape 4).
Les autres catégories (bad-argument-type, missing-attribute, …) sont des bugs de type réels. Traite-les selon où pyrefly les rapporte :
- Rapportés dans un autre fichier (path ≠ cible) : laisse-les. N'élargis pas la portée. Si l'erreur bloque maintenant la cible, supprime-la au site de rapport avec
# pyrefly: ignore[<category>] # TODO. - Rapportés dans le fichier cible mais le message nomme un symbole défini ailleurs (ex.
bad-returnparce qu'une annotation de fonction importée est fausse) : supprime-la localement avec le même commentaire TODO. N'invente pas uncast()qui panse sur l'écart en amont. - Rapportés dans le fichier cible, originaire localement : corrige-le.
Utilise # pyrefly: ignore[...] seulement en dernier recours, et seulement sur les catégories hors-cible.
Étape 4 : Ajouter des annotations
Examine les sites d'appel quand le bon type n'est pas évident à partir du corps de la fonction.
Conventions d'annotation
- Utilise la syntaxe PEP 604 / PEP 585 (
int | None,list[str]) — assume Python >= 3.10. - Préfère
collections.abcàtypingpour les ABCs (Callable,Sequence,Generator, ...). - Pour les helpers génériques, importe de
typingquand disponible sur la version Python minimale du projet, et detyping_extensionsseulement si tu as besoin d'une fonctionnalité plus récente (ex.Selfetoverridesi support < 3.11/3.12, oudefault=PEP 696 pourTypeVar/ParamSpec). N'importe pas en bloc detyping_extensions. - Paramétrise toujours
Callable—Callable[..., Any]quand la signature est vraiment inconnue, jamaisCallablenu. (Voir ParamSpec ci-dessous pour le cas du wrapper préservant la signature.) - Les attributs de classe assignés dans
__init__doivent avoir une annotation au niveau de la classe pour que pyrefly puisse les voir. - Casse les cycles d'import avec
if TYPE_CHECKING:— les imports d'annotations uniquement vont dans la garde, et utilisefrom __future__ import annotations(ou des refs string) pour que les imports runtime restent paresseux :from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from torch.fx import GraphModule def transform(gm: GraphModule) -> GraphModule: ... - Ne supprime jamais les trois catégories cibles.
unannotated-return,unannotated-parameteretimplicit-anysont toujours résolubles en ajoutant une annotation ;# pyrefly: ignore[<l'un de ceux-ci>]n'est pas un résultat acceptable. La seule exception est la dérogation pour la compatibilité descendante ci-dessous. - Élargis, ne renonce pas. Quand le bon type est difficile à déduire, descends cette échelle plutôt que de chercher une suppression :
- Type concret le plus spécifique observable à partir des sites d'appel et des chemins de retour.
- Une union (
X | Y), un type abstrait styleSequence[X], ou unTypeVarlié pour les fonctions véritablement génériques (identity-passthrough, helpers de conteneur). object— meilleur fallback stricte qui type-check quand même. Force les appelants à réduire avant utilisation, ex.def serialize(value: object) -> str:. Visuellement similaire àAnymais plus strict — pyrefly rejettevalue.foo()sans unisinstance.Any— dernier échelon. Toujours préféré à un# pyrefly: ignoresur une catégorie cible, mais seulement après que les échelons 1–3 échouent. Sois capable d'articuler pourquoi chaque échelon antérieur ne convient pas (ex. "union excède 8 types", "aucune limite observable commune", "les appelants ne réduisent jamais vraiment").
- Lis au moins trois sites d'appel avant de décider qu'un paramètre doit être
Any— ne fais pas du pattern-matching "semble dynamique" au premier coup. # pyrefly: ignore[...]à portée étroite (sur une catégorie hors-cible) est réservé aux cas où pyrefly a vraiment tort sur une erreur locale spécifique — métaprogrammation dynamique, écarts dans les stubs tiers :# pyrefly: ignore[attr-defined] result = getattr(obj, dynamic_name)()
Compatibilité descendante (l'une exception au jamais supprimer)
CRITIQUE : Les fonctions décorées avec @compatibility(is_backward_compatible=True) ne doivent PAS avoir leurs signatures changées. Le test de compatibilité descendante (test_function_back_compat) compare inspect.signature stringifié contre un fichier golden — ajouter des annotations (même -> None) change cette string et le test échoue. Utilise les commentaires d'ignore pyrefly à la place :
@compatibility(is_backward_compatible=True)
def my_function( # pyrefly: ignore[unannotated-return]
self,
arg1, # can't add type here either
):
...
Le commentaire # pyrefly: ignore doit être sur la ligne def (où pyrefly rapporte l'erreur), pas sur la fermeture ).
ParamSpec pour les wrappers préservant la signature (décorateurs, helpers style functools.wraps). Utilise Callable[P, R] pour que la signature de la fonction emballée coule à travers vers l'appelant — Callable[..., Any] la perd. Saute ParamSpec si le wrapper accepte vraiment des callables arbitraires. Apparie avec Concatenate[X, P] quand le wrapper préfixe ou suffixe des args.
from collections.abc import Callable
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(fn: Callable[P, R]) -> Callable[P, R]:
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
return fn(*args, **kwargs)
return wrapper
Étape 5 : Itérer
Relance pyrefly check. Les nouvelles annotations surfacent souvent des erreurs bad-return où la fonction retourne en fait un type incompatible — corrige-les. Répète jusqu'à ce que ce soit propre.
Étape 6 : Linter
Obligatoire avant de remettre — les annotations déplacent fréquemment l'ordre des imports et la longueur des lignes :
lintrunner -a <files...>
Résous manuellement ce que lintrunner ne peut pas auto-fixer.
Étape 7 : Tester
Précédence quand quelque chose échoue : tests réussissent > pyrefly propre > strictesse d'annotation. Si une annotation fraîchement ajoutée casse un test, réduis-la d'un échelon dans l'échelle de discipline (ex. concret → object, ou retire un élargissement Any qui a cassé un check isinstance en aval) avant de revert le fichier.
-
Vérification de compatibilité descendante. Lance ssi
grep -l '@compatibility(is_backward_compatible=True)' <target>retourne le fichier — le décorateur est la vraie précondition pour le fichier golden. L'heuristique plus large "importetorch.fx" capture la moitié detorch/.python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v -
Tests unitaires pour le module modifié. Cherche des deux côtés avant de conclure qu'aucune couverture n'existe :
# torch/foo/bar.py est généralement couvert par test/test_foo.py ou test/test_bar.py ls test/ | grep -i <module-name> # ou par import grep -rl "from torch.foo.bar import\|import torch.foo.bar" test/Si les deux sont vides, dis-le à l'utilisateur — ne saute pas silencieusement. Les changements de type peuvent introduire des régressions runtime réelles (
Optional[X]vsX,Sequencevslistquand.appendest appelé, etc.).
Notes
- Refs forward dans les corps de classe sans
from __future__ import annotationsont toujours besoin de quoting en string :class MyClass: def __new__(cls) -> "MyClass": ... - Commiter : ne commit pas sauf si l'utilisateur le demande explicitement (selon le CLAUDE.md du repo). Arrête-toi et expose le diff pour révision quand le fichier est propre.