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 Couverture de types Pyrefly

Prérequis

  • Le fichier doit se trouver 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 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-return parce qu'une annotation de fonction importée est fausse) : supprime-la localement avec le même commentaire TODO. N'invente pas un cast() 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 à typing pour les ABCs (Callable, Sequence, Generator, ...).
  • Pour les helpers génériques, importe de typing quand disponible sur la version Python minimale du projet, et de typing_extensions seulement si tu as besoin d'une fonctionnalité plus récente (ex. Self et override si support < 3.11/3.12, ou default= PEP 696 pour TypeVar / ParamSpec). N'importe pas en bloc de typing_extensions.
  • Paramétrise toujours CallableCallable[..., Any] quand la signature est vraiment inconnue, jamais Callable nu. (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 utilise from __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-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 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 :
    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 style Sequence[X], ou un TypeVar lié pour les fonctions véritablement génériques (identity-passthrough, helpers de conteneur).
    3. 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 à 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 (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.

  1. 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 "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 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] vs X, Sequence vs list quand .append est appelé, etc.).

Notes

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

Skills similaires