INW020 adapter-conformance¶
Co robi¶
Opt-in, a po włączeniu błąd. Sprawdza, czy każda publiczna klasa najwyższego poziomu w module adaptera deklaruje port, który implementuje, linią zgodności: przypisaniem z adnotacją w bloku if TYPE_CHECKING: (albo if typing.TYPE_CHECKING:) na poziomie modułu, którego wartość buduje tę klasę albo jest tą klasą.
if TYPE_CHECKING:
_: OrderRepository = SqlOrderRepository(...) # or
_: type[OrderRepository] = SqlOrderRepository
Nazwa zmiennej jest dowolna; konwencją jest _. Argumenty mogą być dowolne, także ..., i liczy się też SqlOrderRepository[int](...). Zgłaszane są trzy rzeczy:
- Brak linii zgodności, na nazwie klasy. Przykładowa linia w komunikacie nazywa port, gdy moduł importuje dokładnie jedną klasę z modułu portu, a w przeciwnym razie pisze
<Port>. - Linia, która się wykonuje, na tej linii: to samo przypisanie z adnotacją na poziomie modułu poza
if TYPE_CHECKING:, także w gałęzielse. Przy każdym imporcie modułu buduje adapter albo wymaga, żeby port był importowany w czasie działania. - Adnotacja, która nie nazywa portu, na adnotacji: gdy ustawiono
ports, klasa spoza tych modułów; zportsi bez,Anyalboobject, które przyjmują każdą wartość.
Klasa nie potrzebuje linii, gdy jej nazwa zaczyna się od _, gdy sama jest portem (Protocol albo ABC) albo gdy jest rekordem: wyjątkiem, enumem, modelem Pydantic, named tuple, typed dict albo dataclassą. Zobacz Konfiguracja.
Bez modules reguła sprawdza każdy moduł z segmentem adapters w nazwie: shop.adapters, shop.adapters.outbound.orders. Ustaw modules, żeby sprawdzać inne moduły.
Dlaczego to źle¶
Port to tutaj typing.Protocol, a adapter implementuje go, mając jego składowe; dziedziczenie po protokole „nie jest konieczne dla sprawdzania typów” (specyfikacja typowania, Protocols). W module adaptera nic więc nie mówi, jaki port implementuje, i nic tego nie sprawdza, dopóki korzeń kompozycji nie przekaże adaptera tam, gdzie oczekiwany jest port, o ile w ogóle to zrobi. Agent, który zmieni nazwę metody portu albo doda nową, dowie się o tym dopiero w czasie działania.
Linia zgodności zamyka tę lukę. TYPE_CHECKING ma wartość False w czasie działania, a statyczne type checkery przyjmują True (typing.TYPE_CHECKING), więc linia niczego nie buduje, a mimo to jest sprawdzana: przypisywalność do protokołu jest strukturalna, a ty zgłasza wartość niepasującą do zadeklarowanego typu jako invalid-assignment, tak samo jak mypy i Pyright. Zmienna z adnotacją type[Port] przyjmuje klasę konkretną, której instancje implementują protokół (type[] and class objects vs protocols). INW020 sprawdza, czy linia istnieje; type checker sprawdza, co ona mówi (ADR-046).
Przykład¶
[tool.inwards]
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "adapters", modules = ["shop.adapters"] },
]
[tool.inwards.rules]
extend-select = ["INW020"]
[tool.inwards.rules.adapter-conformance]
ports = ["shop.application.ports"]
from typing import Protocol
class OrderRepository(Protocol):
def get(self, order_id: int) -> dict: ...
from typing import TYPE_CHECKING
from shop.application.ports.orders import OrderRepository
from shop.domain.orders import Order
class StorageError(Exception):
pass
class SqlOrderRepository:
def get(self, order_id: int) -> dict:
return {"id": order_id}
class CachedOrderRepository:
def get(self, order_id: int) -> dict:
return {"id": order_id}
_: OrderRepository = SqlOrderRepository()
if TYPE_CHECKING:
__: Order = CachedOrderRepository()
shop/adapters/orders.py:21:1: INW020 `_: OrderRepository = SqlOrderRepository()` in adapter module `shop.adapters.orders` is outside `if TYPE_CHECKING:`, so it runs when the module is imported and builds a `SqlOrderRepository`. A conformance line is only for the type checker.
fix: Move the line into an `if TYPE_CHECKING:` block at the end of the module.
1. Move the line into an `if TYPE_CHECKING:` block at module level (`from typing import TYPE_CHECKING`), at the end of the module.
2. Move the port's import into the same block if nothing else in the module uses it at runtime.
docs: https://sircypkowskyy.github.io/inwards/rules/INW020/
shop/adapters/orders.py:24:9: INW020 The conformance line for `CachedOrderRepository` in adapter module `shop.adapters.orders` is annotated `shop.domain.orders.Order`, which isn't a class from a port module (`ports`), so the type checker doesn't check the class against a port.
fix: Annotate the line with the port `CachedOrderRepository` implements.
1. Replace `Order` with the port `CachedOrderRepository` implements, imported from its port module under `if TYPE_CHECKING:`.
2. If that port lives outside the modules `ports` lists in [tool.inwards.rules.adapter-conformance], ask the user to add its module there. Don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW020/
StorageError przechodzi, bo jest wyjątkiem. Po poprawce obie linie są pod if TYPE_CHECKING: i mają adnotację z portem.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from shop.application.ports.orders import OrderRepository
class StorageError(Exception):
pass
class SqlOrderRepository:
def get(self, order_id: int) -> dict:
return {"id": order_id}
class CachedOrderRepository:
def get(self, order_id: int) -> dict:
return {"id": order_id}
if TYPE_CHECKING:
_: OrderRepository = SqlOrderRepository()
__: type[OrderRepository] = CachedOrderRepository
Jak działa¶
Plik jest parsowany tylko wtedy, gdy INW020 jest włączona dla jego modułu, moduł jest modułem adaptera, a w tekście jest słowo class. Jedno parsowanie na taki plik; żaden inny moduł nie jest czytany, więc hook po każdej edycji, edytor i MCP zgłaszają ją przy edycji, która ją psuje.
- Klasy. Klasy najwyższego poziomu, których nazwa nie zaczyna się od
_. Klasa jest zwolniona, gdy ma dozwolony dekorator albo bazę, gdy jej bazą jest klasa z tego samego pliku zwolniona w ten sposób, albo gdy wymieniaProtocol,abc.ABClubmetaclass=abc.ABCMeta. - Nazwy są kwalifikowane przez importy pliku, tak jak w INW014: liczą się
import typingztyping.TYPE_CHECKING, importy z aliasem ityping_extensions. Nazwa, której plik nie importuje, to nazwa wbudowana, pisana bez prefiksu (Exception,object). - Linie. Przypisanie z adnotacją bezpośrednio na poziomie modułu albo bezpośrednio w gałęzi
ifna poziomie modułu. Jego wartość toName(...)alboName[...](...)z dowolną adnotacją, albo samoNamepodtype[...](takżetyping.Type[...]), gdzieNameto klasa, którą reguła sprawdza. Linia w cieleif TYPE_CHECKING:alboelif TYPE_CHECKING:jest dla type checkera; każda inna się wykonuje. - Porty. Gdy ustawiono
ports, głowa adnotacji (RepozRepo[int]) musi wskazywać nazwę w stylu CapWords zdefiniowaną w jednym z tych modułów. Bez tej opcji port w komunikacie to jedyna nazwa CapWords, którą moduł importuje z modułu z segmentemports. - Jedno zgłoszenie na problem. Klasa z linią, która się wykonuje, albo z linią, której adnotacja nie nazywa portu, nie jest dodatkowo zgłaszana jako klasa bez linii.
Jak naprawić¶
- Zaimportuj port pod
if TYPE_CHECKING:. - Dodaj
_: Port = Adapter(...)albo_: type[Port] = Adapterw blokuif TYPE_CHECKING:na końcu modułu; dla klasy, która implementuje kilka portów, jedną linię na port. - Jeśli klasa jest pomocnicza, a nie jest adapterem, zmień jej nazwę na
_Namealbo przenieś ją poza moduł adaptera. Jeśli jest rekordem na własnej bazie projektu, poproś osobę odpowiedzialną za konfigurację o dodanie tej bazy doextend-allow-bases.
Bezpieczeństwo poprawki¶
Brak automatycznej poprawki (autofix: false). Tego, jaki port implementuje klasa, nie ma w kodzie; właśnie to mówi linia.
Konfiguracja¶
Ważność: błąd. Opt-in: włącz ją przez extend-select. Presety clean i hexagonal polecenia inwards init włączają INW020 jako ostrzeżenie, z modules ustawionym na warstwę adapterów wyjściowych (driven) i ports na application.ports. Opcje są w [tool.inwards.rules.adapter-conformance] (tabele opcji):
[tool.inwards.rules]
extend-select = ["INW020"]
[tool.inwards.rules.adapter-conformance]
modules = ["shop.infrastructure"] # optional: the adapter modules
ports = ["shop.application.ports"] # optional: annotations must name a class from these
extend-allow-bases = ["shop.kit.Schema"]
modules: prefiksy modułów albo selektory, jak wmoduleswarstwy. Gdy jest ustawione, INW020 sprawdza pasujące moduły zamiast tych z segmentemadapters.ports: prefiksy albo selektory modułów portów. Gdy jest ustawione, adnotacja linii musi nazywać klasę z jednego z nich, a komunikat nazywa tę, którą moduł z nich importuje.allow-bases,extend-allow-basesiallow-decorators: jak w INW014, z tymi samymi wartościami domyślnymi (wyjątki,pydantic.BaseModel,NamedTuple,TypedDict,enum.*; dekoratory dataclass, pydantic i attrs).check-tests:truesprawia, że INW020 zgłasza też w modułach testowych, które domyślnie pomija (tabele opcji).
Żeby zaakceptować jedno zgłoszenie, wycisz je w linii, na którą wskazuje: # inwards: ignore[INW020] reason="..." w linii class, w linii zgodności albo w linii adnotacji.
Szablon może włączyć INW020 dla jednej ze swoich ról: adapters = { adapter-conformance = true } ustawia modules na moduły roli.
Znane ograniczenia¶
- Adapter czy klasa pomocnicza. Reguła ich nie odróżnia: każda publiczna klasa w module adaptera potrzebuje linii. Klasa pomocnicza potrzebuje podkreślenia, dozwolonej bazy albo węższego
modules. - Bazy projektu. Model Pydantic na własnej bazie projektu (
class Cursor(Schema)zSchemazpolar.kit.schemas) jest zgłaszany, dopóki tej bazy nie ma wextend-allow-bases: baza jest czytana po nazwie, a nie śledzona. - Co mówi linia. To, czy klasa pasuje do portu, sprawdza type checker, a w projekcie, który go nie uruchamia, linii nikt nie sprawdza.
- Czytane linie. Linia zagnieżdżona głębiej niż gałąź
ifna poziomie modułu (wtryalbo wifwewnątrzif TYPE_CHECKING:) nie jest czytana. Adnotacja w cudzysłowie (_: "Port" = Adapter(...)) liczy się jako linia, ale jej port nie jest sprawdzany. - Porty w stylu CapWords. Port w komunikacie i sprawdzanie
portstraktują nazwę CapWords jako klasę, a nazwę małymi literami jako moduł, bo żaden inny plik nie jest czytany. - Pomiar na korpusie (2026-10-11, pięć repozytoriów z
bench/corpus.json, warstwy z manifestu plusselect = ["INW020"]): 8 zgłoszeń w fastapi-clean-example, czyli jego 8 adapterów, które zamiast tego dziedziczą po portach; 2 w Polarze, jegoStripeAdapteriStripeExtractionCursor, model Pydantic na własnej bazie projektuSchema; żadnego w pozostałych trzech, które nie mają modułówadapters. Żaden projekt w korpusie nie stosuje jeszcze tej konwencji, dlatego reguła jest opt-in.
Źródła¶
- #433: reguła; ADR-046 i #407: decyzja i spike.
- Specyfikacja typowania: Protocols: strukturalna przypisywalność, jawne dziedziczenie i
type[Proto]. typing.TYPE_CHECKING:Falsew czasie działania,Truedla statycznych type checkerów.- ty:
invalid-assignment: sprawdzenie, które czyta linię.