pyrefly-type-coverage

Par pytorch · pytorch

Migrez un fichier pour utiliser la vérification de types Pyrefly plus stricte, avec des annotations obligatoires pour toutes les fonctions, classes et attributs.

npx skills add https://github.com/pytorch/pytorch --skill pyrefly-type-coverage

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-return parce que l'annotation d'une fonction importée est fausse) : supprime-la localement avec le même commentaire TODO. N'invente pas de cast() 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 à typing pour les ABCs (Callable, Sequence, Generator, ...).
  • Pour les aides génériques, importe depuis typing quand disponible sur la version Python minimale du projet, et depuis typing_extensions uniquement quand tu as besoin d'une fonctionnalité plus récente (par ex., Self et override si tu supports < 3.11/3.12, ou PEP 696 default= pour TypeVar / ParamSpec). N'importe pas en masse depuis typing_extensions.
  • Paramétre toujours Callable (jamais un Callable brut). Préfère Callable[..., object] ; ne vise Callable[..., 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é), object est 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 (_P surpasse P ~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 (souvent object), retourne bool — veut généralement TypeGuard[X] (ou TypeIs[X], qui rétrécit aussi la branche négative). TypeGuard est dans typing (>= 3.10, donc importe de là) ; TypeIs n'est entré dans typing qu'en 3.13, donc importe-le depuis typing_extensions (>= 4.10) pour rester compatible 3.10. Un aide de style issubclass prenant klass: type[_T] devrait retourner TypeGuard[type[_T]]. Préfère une garde isinstance(x, type) explicite à try/except TypeError autour de issubclass() — 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] avec ParamSpec/TypeVar) plutôt que d'élargir à object/Any. "Type de sortie == un certain type d'entrée" est exactement ce qu'un TypeVar code ; object in / object out 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 simple TypeVar est 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 utilise from __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, et implicit-any sont 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 :
    1. Type concret le plus spécifique observable à partir des sites d'appel et des chemins de retour.
    2. Une union (X | Y), un type abstrait de style Sequence[X], ou un TypeVar borné pour les vraies fonctions génériques (identité-passthrough, aides conteneur).
    3. 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 à Any mais plus strict — pyrefly rejette value.foo() sans un isinstance.
    4. Any — dernier échelon. Toujours préféré à un # pyrefly: ignore sur 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/Any en 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 battent object).
  • 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: skip pour 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 ligne def.)

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.

  1. 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 "importe torch.fx" capture la moitié de torch/.

    python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v
  2. 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] vs X, Sequence vs list quand .append est appelée, etc.).

Notes

  • Refs forward dans les corps de classe sans from __future__ import annotations ont 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.

Skills similaires