pyrefly-type-coverage

pyrefly-type-coverage

熱門

將單一檔案遷移至使用更嚴格的 Pyrefly 型別檢查,要求所有函式、類別與屬性皆須加上型別標註。

10萬星標
2.9萬分支
更新於 2026/8/4
SKILL.md
唯讀
名稱
pyrefly-type-coverage
描述

將單一檔案遷移至使用更嚴格的 Pyrefly 型別檢查,要求所有函式、類別與屬性皆須加上型別標註。

Pyrefly 型別覆蓋率 Skill

前置條件

  • 該檔案必須位於包含 pyrefly.toml 的專案中。
  • pyreflylintrunner 以及專案的測試執行工具(test runner)必須已加入 PATH。若缺少其中任何一項,請停止執行並詢問是否需要啟用 conda 環境 — 切勿自行安裝或替換(依據專案 repo 的 CLAUDE.md 規範)。

步驟 1:移除檔案層級的型別檢查抑制(suppression)

從檔案頂部刪除以下任何註解(pyrefly 為了相容 mypy 會採納 # mypy: ignore-errors,因此該項也必須移除):

# pyre-ignore-all-errors
# pyre-ignore-all-errors[16,21,53,56]
# @lint-ignore-every PYRELINT
# mypy: ignore-errors

步驟 2:在 pyrefly.toml 中新增子設定區段(sub-config)

[[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

重要事項:在 [sub-config.errors] 中設定任何錯誤鍵值(error key),只會相對於父層設定覆寫該鍵值 — 但若啟用 unannotated-return / unannotated-parameter / implicit-any,原本在全檔範圍被隱藏的錯誤將會重新浮現。若您看到無關的錯誤(例如 bad-param-name-override)大量洗版輸出,請在子設定中同步父層設定對該鍵值的設定值以消音。

步驟 3:執行 pyrefly

pyrefly check <FILENAME>

目標: 透過新增型別標註來解決所有 unannotated-returnunannotated-parameterimplicit-any 錯誤 — 詳情請參閱步驟 4 的階梯原則。這三個目標類別總是能夠被解決的;絕不要使用 # pyrefly: ignore 來抑制它們。唯一的例外是 @compatibility(is_backward_compatible=True)(步驟 4)。

其他類別(bad-argument-typemissing-attribute 等)屬於真實的型別 Bug。請根據 pyrefly 報告錯誤的位置來處置:

  • 於其他檔案回報(路徑 != 目標檔案):保持原樣,勿擴大變更範圍。若該錯誤因而阻礙了目標檔案的處理,請在回報出處使用 # pyrefly: ignore[<category>] # TODO 進行抑制。
  • 於目標檔案回報,但訊息指向別處定義的符號(例如因匯入函式的標註有誤而導致 bad-return):在本地使用相同的 TODO 註解予以抑制。切勿憑空加上 cast() 來掩蓋上游缺口。
  • 於目標檔案回報,且起源於本地:直接修復。

僅將 # pyrefly: ignore[...] 作為最後手段,且僅限用於非目標類別。

步驟 4:新增型別標註

當無法直接從函式主體推斷出正確型別時,請檢查呼叫處(call sites)。

型別標註慣例
  • 使用 PEP 604 / PEP 585 語法(int | Nonelist[str]) — 假設 Python >= 3.10。
  • 對於抽象基底類別(ABCs,如 CallableSequenceGenerator ...),優先使用 collections.abc 而非 typing
  • 對於泛型輔助工具(generic helpers),只要專案支援的最低 Python 版本有提供,就從 typing 匯入;僅在需要較新功能時才從 typing_extensions 匯入(例如支援 < 3.11/3.12 時所需的 Selfoverride,或 PEP 696 中 TypeVar / ParamSpecdefault=)。請勿一律盲目從 typing_extensions 匯入。
  • 務必為 Callable 加上參數型別(絕不要使用裸露的 Callable)。優先考慮 Callable[..., object];只有在呼叫方確實會使用動態回傳值時,才使用 Callable[..., Any] — 若回傳結果只是被原封不動傳遞(或該可呼叫物件甚至未被呼叫),使用 object 更為嚴謹且同樣正確。(若為需要保留簽章的包覆器(wrapper)情境,請參閱下方的 ParamSpec 部分。)
  • 為您所新增的任何模組區域全域變數開頭加上底線 — 包括 TypeVar/ParamSpec(需與字串引數一致:_T = TypeVar("_T")_P = ParamSpec("_P")_R = TypeVar("_R"))、TypeAlias、輔助常數以及 Sentinel。這是 torch 處理非公開名稱的主流慣例(在程式碼樹中 _P 的數量約為 P 的 6 倍)。例外情況(不加前綴底線):會被其他模組匯入的名稱、已列於 __all__ 中、或用作執行期 Token 者(例如標註字串派發標記)。此規則僅適用於您新增的名稱 — 切勿重新命名已存在的全域變數,那是超出本 Skill 範疇的無關重構。
  • 布林判斷式(boolean predicate) — 命名為 is_*/has_*、接收寬鬆型別(通常為 object)並回傳 bool — 通常適合使用 TypeGuard[X](或 TypeIs[X],後者還能窄化否定分支)。TypeGuard 已收錄於 typing(>= 3.10,故直接自該處匯入);TypeIs 則是在 3.13 才加入 typing,因此需自 typing_extensions(>= 4.10)匯入以維持對 3.10 的相容性。接收 klass: type[_T]issubclass 風格輔助函式應回傳 TypeGuard[type[_T]]。相較於用 try/except TypeError 包裹 issubclass(),優先使用顯式的 isinstance(x, type) 檢查防護 — 這樣更清晰,且能讓型別檢查器進行窄化推導。
  • 當回傳型別是衍生自某個參數時 — 例如原樣傳遞/恆等函式(passthrough/identity functions)、「回傳傳入引數之一」的輔助函式、裝飾器(decorators)、以型別為 Key 的註冊表(registries) — 請使用 TypeVar(或者針對簽章需要傳遞的可呼叫引數,搭配 ParamSpec/TypeVar 使用 Callable[_P, _R]),而非放寬為 object/Any。「輸出型別 == 某個輸入型別」正是 TypeVar 所表達的核心含義;使用 object 輸入 / object 輸出會丟失此資訊。注意事項:若函式會轉化該值以致輸出型別與輸入不同(例如將陣列轉換為整數),使用單一 TypeVar 是錯誤的 — 此時應直接指定實際的領域型別(domain type)。
  • __init__ 中指派的類別屬性應加上類別層級的型別標註,以便 pyrefly 能夠識別。
  • 使用 if TYPE_CHECKING: 來打破循環匯入(import cycles) — 僅用於標註的匯入應放在檢查條件內,並使用 from __future__ import annotations(或字串形式的前向引用 forward refs)以保持執行期匯入的延遲載入特性:
    from __future__ import annotations
    from typing import TYPE_CHECKING
    if TYPE_CHECKING:
        from torch.fx import GraphModule
    def transform(gm: GraphModule) -> GraphModule: ...
    
  • 絕不要抑制那三個目標類別。 unannotated-returnunannotated-parameterimplicit-any 總是能透過新增標註來解決;使用 # pyrefly: ignore[<其中之一>] 是無法接受的結果。唯一的例外是下方的「向下相容例外條款」。
  • 放寬型別,而非放棄。 當難以推斷正確型別時,請依據以下階梯原則逐級向下嘗試,而不是直接加上 ignore 註解:
    1. 從呼叫處與回傳路徑中可觀察到的最具體型別(concrete type)。
    2. 聯集型別(X | Y)、Sequence[X] 風格的抽象型別,或是針對真正通用泛型函式(原樣傳遞、容器輔助工具)的受限 TypeVar(bound TypeVar)。
    3. object — 能通過型別檢查的最嚴格退路。強迫呼叫方在使用前必須先窄化型別,例如 def serialize(value: object) -> str:。視覺上與 Any 相似但更為嚴格 — pyrefly 在沒有 isinstance 檢查的情況下會拒絕 value.foo()
    4. Any — 最底層階梯。在目標類別上總是優先於使用 # pyrefly: ignore,但前提是第 1–3 階梯皆已嘗試失敗。必須能夠清楚說明為何先前各階梯皆不適用(例如:「聯集超過 8 種型別」、「無法觀察到共同邊界」、「呼叫方確實從不窄化」)。
  • 回傳位置使用 object/Any 時要特別謹慎 — 函式自身通常比其呼叫方更清楚自己產出的內容。只有在真正的邊界處(例如原封不動回傳輸入值,或該值是由處理常式/呼叫方所定義),寬鬆的回傳型別才是正確的;若函式主體建構的是已知結構,請明確指定名稱(使用領域別名或聯集型別皆勝過 object)。
  • 在決定將參數設為 Any 之前,請至少閱讀三個呼叫處 — 切勿在第一次嘗試時僅憑「看起來很動態」就做出判斷。
  • 窄範圍的 # pyrefly: ignore[...](用於非目標類別)僅保留給 pyrefly 對特定局部錯誤確實判斷錯誤的情況 — 例如動態元程式設計(dynamic metaprogramming)、第三方存根檔(stub)缺失等:
    # pyrefly: ignore[attr-defined]
    result = getattr(obj, dynamic_name)()
    
  • 當單行內的 # pyrefly: ignore[...] 會導致行長超過限制時,請將其放置於被標記行的正上方一行,而非使用 # fmt: skip 強行留在同行 — pyrefly 能識別上一行的 ignore 註解。(例外:下方的向下相容例外條款,其必須位於 def 行)。
向下相容性(絕不抑制規則的唯一例外)

關鍵重要:標有 @compatibility(is_backward_compatible=True) 裝飾器的函式絕不可變更其函數簽章(signature)。向下相容性測試(test_function_back_compat)會將轉為字串的 inspect.signature 與 Baseline 檔(golden file)進行比對 — 新增標註(即使是 -> None)都會改變該字串並導致測試失敗。此時請改用 pyrefly ignore 註解:

@compatibility(is_backward_compatible=True)
def my_function(  # pyrefly: ignore[unannotated-return]
    self,
    arg1,  # can't add type here either
):
    ...

# pyrefly: ignore 註解必須位於 def 那一行(即 pyrefly 報告錯誤的位置),而非位於結尾的 ) 行。

用於保留簽章之包覆器的 ParamSpec(裝飾器、functools.wraps 風格的輔助函式)。請使用 Callable[P, R],使被包裹函式的簽章能透傳給呼叫方 — 若使用 Callable[..., Any] 則會丟失簽章資訊。若該包覆器確實可接收任意 Callables,則可跳過 ParamSpec。當包覆器會在參數最前或最後附加引數時,請搭配 Concatenate[X, P] 使用。

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

步驟 5:反覆迭代

重新執行 pyrefly check。新增的型別標註往往會浮現出 bad-return 錯誤(即函式實際上回傳了不相容的型別) — 請修復這些錯誤。重複此過程直到檢查完全通過(clean)。

限縮或嚴格化共享輔助函式(例如新增 TypeGuard 或更精準的回傳型別)可能會導致其呼叫方原本存在的 # pyrefly: ignore 註解失效。請重新檢查並刪除已無用的抑制註解與過時的說明文字 — 切勿保留。

步驟 6:程式碼檢查(Lint)

在交卷前為必要步驟 — 新增型別標註經常會改變匯入順序與行長度:

lintrunner -a <files...>

若有 lintrunner 無法自動修復的項目,請手動予以解決。

步驟 7:測試

發生失敗時的優先順序:測試通過 > pyrefly 檢查乾淨 > 型別標註嚴謹度。若新加入的型別標註導致測試失敗,請在還原檔案前,依據階梯原則退回一個階梯(例如從具體型別退至 object,或移除破壞了下游 isinstance 檢查的 Any 放寬)。

  1. 向下相容性檢查。 當且僅當 grep -l '@compatibility(is_backward_compatible=True)' <target> 回傳該檔案時才執行 — 該裝飾器才是 Baseline 檔的真正前置條件。較為寬鬆的「匯入 torch.fx」啟發式檢查會誤涵蓋半數的 torch/ 檔案。

    python -m pytest test/test_fx.py::TestFXAPIBackwardCompatibility -x -v
    
  2. 受修改模組的單元測試。 在判定「不存在測試覆蓋」之前,請雙向搜尋比對:

    # torch/foo/bar.py is usually covered by test/test_foo.py or test/test_bar.py
    ls test/ | grep -i <module-name>
    # or by import
    grep -rl "from torch.foo.bar import\|import torch.foo.bar" test/
    

    若兩者皆查無結果,請告知使用者 — 切勿默默跳過。型別變更有可能引入真實的執行期退化(runtime regressions,例如 Optional[X]X 的差異,或當呼叫 .appendSequencelist 的差異等)。

補充說明

  • 類別主體中的前向引用(Forward refs):若未加上 from __future__ import annotations,仍需使用字串引號包裹:
    class MyClass:
        def __new__(cls) -> "MyClass": ...
    
  • 提交(Committing):除非使用者明確要求,否則請勿 commit(依據專案 repo 的 CLAUDE.md 規範)。當檔案通過檢查後,請停止操作並輸出 diff 供審閱。