Compétence Pyrefly Type Coverage
Prérequis
- Le fichier doit vivre dans un projet avec un
pyrefly.toml. pyrefly,lintrunner, et 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 l'installe pas et ne le remplace pas (selon le CLAUDE.md du dépôt).
Étape 1 : Supprimer les suppressions de vérification de type au niveau du fichier
Supprime n'importe lequel de ceux-ci du haut du fichier (pyrefly honore # mypy: ignore-errors pour la compatibilité mypy, celui-là doit donc disparaître aussi) :
# 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 sub-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] n'override que cette clé par rapport au parent — mais activer unannotated-return / unannotated-parameter / implicit-any fera réapparaître les erreurs qui étaient précédemment masquées au niveau du fichier. Si tu vois des erreurs sans rapport (par ex., bad-param-name-override) inonder la sortie, reproduis le réglage de la config parent pour cette clé dans la sub-config pour les faire taire.
É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 de l'étape 4. Ces trois catégories cibles sont toujours résolubles ; ne les supprime jamais avec # pyrefly: ignore. La seule exception est @compatibility(is_backward_compatible=True) (étape 4).
Les autres catégories (bad-argument-type, missing-attribute, …) sont de vrais bugs de type. Traite-les selon où pyrefly les rapporte :
- Rapportées 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 du rapport avec
# pyrefly: ignore[<category>] # TODO. - Rapportées dans le fichier cible mais le message nomme un symbole défini ailleurs (par ex.,
bad-returnparce que l'annotation d'une fonction importée est fausse) : supprime-la localement avec le même commentaire TODO. N'invente pas decast()qui cache le décalage en amont. - Rapportées dans le fichier cible, originaires localement : corrige-la.
Utilise # pyrefly: ignore[...] uniquement en dernier recours, et seulement sur des catégories non-cibles.
É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]) — suppose Python >= 3.10. - Préfère
collections.abcàtypingpour les ABCs (Callable,Sequence,Generator, ...). - Pour les aides génériques, importe depuis
typingquand disponible sur la version Python minimale du projet, et depuistyping_extensionsuniquement quand tu as besoin d'une fonctionnalité plus récente (par ex.,Selfetoverridesi tu supports < 3.11/3.12, ou PEP 696default=pourTypeVar/ParamSpec). N'importe pas en masse depuistyping_extensions. - Paramétre toujours
Callable(jamais unCallablebrut). PréfèreCallable[..., object]; ne viseCallable[..., Any]que quand un appelant consomme véritablement le retour dynamique — si le résultat est juste passé (ou que le callable n'est même pas invoqué),objectest plus strict et tout aussi correct. (Voir ParamSpec ci-dessous pour le cas du wrapper préservant la signature.) - Donne à tout global au niveau du module que tu introduis un tiret bas initial —
TypeVar/ParamSpec(assortis à l'argument chaîne :_T = TypeVar("_T"),_P = ParamSpec("_P"),_R = TypeVar("_R")),TypeAliases, constantes d'aide, et sentinelles du même acabit. C'est la convention torch prévalente pour les noms non-publics (_PsurpasseP~6:1 dans l'arbre). Exceptions (laisser sans tiret bas) : un nom importé par d'autres modules, listé dans__all__, ou utilisé comme jeton runtime (par ex. un marqueur de dispatch en chaîne d'annotation). S'applique uniquement aux noms que tu ajoutes — ne renomme pas les globals pré-existants ; c'est une refonte sans rapport hors de la portée de cette compétence. - Un prédicat booléen — nom
is_*/has_*, prend un type large (souventobject), retournebool— veut généralementTypeGuard[X](ouTypeIs[X], qui rétrécit aussi la branche négative).TypeGuardest danstyping(>= 3.10, donc importe de là) ;TypeIsn'est entré danstypingqu'en 3.13, donc importe-le depuistyping_extensions(>= 4.10) pour rester compatible 3.10. Un aide de styleissubclassprenantklass: type[_T]devrait retournerTypeGuard[type[_T]]. Préfère une gardeisinstance(x, type)explicite àtry/except TypeErrorautour deissubclass()— plus clair, et ça permet au vérificateur de rétrécir. - Quand un type de retour est dérivé d'un paramètre — passthroughs/fonctions identité, aides "retourner l'un de ces args", décorateurs, registres indexés par type — vise un
TypeVar(ou, pour un arg callable dont la signature s'écoule à travers,Callable[_P, _R]avecParamSpec/TypeVar) plutôt que d'élargir àobject/Any. "Type de sortie == un certain type d'entrée" est exactement ce qu'unTypeVarcode ;objectin /objectout le descarte. Attention : si la fonction transforme la valeur pour que le type de sortie diffère de l'entrée (par ex. convertit un tableau en int), un simpleTypeVarest erroné — nomme plutôt le type de domaine réel. - Les attributs de classe affectés dans
__init__doivent obtenir une annotation au niveau de la classe pour que pyrefly puisse les voir. - Casse les cycles d'import avec
if TYPE_CHECKING:— les importations annotation-uniquement vont dedans la garde, et utilisefrom __future__ import annotations(ou des refs forward en chaîne) pour que les importations runtime restent lazy :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-parameter, etimplicit-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 de compatibilité rétroactive ci-dessous. - Élargis, ne démissionne pas. Quand le bon type est difficile à déduire, descends cette échelle plutôt que de viser 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 de styleSequence[X], ou unTypeVarborné pour les vraies fonctions génériques (identité-passthrough, aides conteneur). object— fallback le plus strict qui type-check encore. Force les appelants à rétrécir avant utilisation, par 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 (par ex., "l'union dépasse 8 types", "pas de borne commune observable", "les appelants ne rétrécissent jamais").
- Sois particulièrement vigilant avec
object/Anyen position de retour — une fonction connaît généralement mieux ce qu'elle produit que ses appelants. Un retour large est juste uniquement à une véritable limite (il retourne son entrée inchangée, ou la valeur est définie par le gestionnaire/appelant) ; si le corps construit une forme connue, nomme-la (un alias de domaine ou une union battentobject). - Lis au moins trois sites d'appel avant de décider qu'un paramètre doit être
Any— ne fais pas de correspondance de motif "semble dynamique" au premier essai. - La portée-restreinte
# pyrefly: ignore[...](sur une catégorie non-cible) est réservée aux cas où pyrefly se trompe réellement sur une erreur locale spécifique — métaprogrammation dynamique, lacunes de stub tiers :# pyrefly: ignore[attr-defined] result = getattr(obj, dynamic_name)() - Quand un
# pyrefly: ignore[...]inline pousserait une ligne au-delà de la limite de longueur, mets-le sur la ligne immédiatement au-dessus de la ligne signalée plutôt que de viser# fmt: skippour le garder inline — pyrefly honore une suppression ligne-précédente. (Exception : la dérogation compatibilité rétroactive ci-dessous, où elle doit être sur la lignedef.)
Compatibilité rétroactive (la seule exception à ne jamais supprimer)
CRITIQUE : Les fonctions décorées avec @compatibility(is_backward_compatible=True) ne doivent pas avoir leurs signatures modifiées. Le test compatibilité-rétroactive (test_function_back_compat) compare inspect.signature stringifiée contre un fichier golden — ajouter des annotations (même -> None) change cette chaîne et le test échoue. Utilise des 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 le ) fermant.
ParamSpec pour les wrappers préservant la signature (décorateurs, aides de style functools.wraps). Utilise Callable[P, R] pour que la signature de la fonction emballée s'écoule jusqu'à l'appelant — Callable[..., Any] la perd. Saute ParamSpec si le wrapper accepte véritablement des callables arbitraires. Associe-le à Concatenate[X, P] quand le wrapper prépend ou ajoute 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 réalité un type incompatible — corrige-les. Répète jusqu'à ce que ce soit clean.
Resserrer une aide partagée (par ex. ajouter un TypeGuard ou un retour précis) peut rendre inutiles les commentaires # pyrefly: ignore pré-existants dans ses appelants. Revérifie et supprime les suppressions maintenant mortes et tout commentaire explicatif obsolète — ne les laisse pas.
Étape 6 : Lint
Obligatoire avant de remettre — les annotations décalent fréquemment l'ordre d'import et la longueur de ligne :
lintrunner -a <files...>
Résous manuellement tout ce que lintrunner ne peut pas auto-fixer.
Étape 7 : Tester
Préséance quand quelque chose échoue : tests passants > pyrefly clean > strictesse d'annotation. Si une annotation nouvellement ajoutée casse un test, rétrécis-la d'un échelon dans l'échelle de discipline (par ex., concret → object, ou supprime un élargissement Any qui a cassé une vérification isinstance en aval) avant de revenir sur le fichier.
-
Vérification compatibilité-rétroactive. Exécute iff
grep -l '@compatibility(is_backward_compatible=True)' <target>retourne le fichier — le décorateur est la vraie condition préalable 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 les deux façons avant de conclure qu'aucune couverture 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 reviennent vides, dis à l'utilisateur — ne saute pas silencieusement. Les changements de type peuvent introduire de vrais régressions runtime (
Optional[X]vsX,Sequencevslistquand.appendest appelée, etc.).
Notes
- Refs forward dans les corps de classe sans
from __future__ import annotationsont encore besoin de quoting en chaîne :class MyClass: def __new__(cls) -> "MyClass": ... - Committer : ne committe pas à moins que l'utilisateur le demande explicitement (selon le CLAUDE.md du dépôt). Arrête-toi et surface le diff pour révision quand le fichier est clean.