INW004 import-cycles¶
Co robi¶
Zgłasza moduły, które importują się nawzajem w cyklu, i konteksty, które to robią. cycles wybiera, które:
"contexts", domyślnie: cykl między kontekstami, złożony z importów z jednego kontekstu do innego.depends-onmoże pozwalać na oba kierunki, a INW004 i tak go zgłasza. Bez kontekstów niczego nie znajduje."modules": cykl między modułami, na przykładorderimportujepricing,pricingimportujediscounts, adiscountsimportujeorder.[]wyłącza regułę.
Każda silnie spójna grupa modułów albo kontekstów jest zgłaszana raz: najkrótszy cykl przez jej pierwszy element, z całą ścieżką, przy imporcie, który robi pierwszy krok, razem z liczbą elementów i powiązań w grupie oraz skrótem (hashem) tych powiązań. Poprawka wymienia import stojący za każdym krokiem.
Liczą się importy w funkcjach, a także importy dynamiczne, których cel Inwards potrafi odczytać. Import w treści if TYPE_CHECKING: albo elif TYPE_CHECKING: (także typing.TYPE_CHECKING) się nie liczy: TYPE_CHECKING ma w czasie działania wartość False, więc ten import nigdy się nie wykonuje, a tak właśnie kod w Pythonie zachowuje podpowiedź typu bez cyklu w czasie działania. Gałąź else takiego bloku się wykonuje, więc import w niej się liczy, tak samo jak import pod if not TYPE_CHECKING: albo pod warunkiem w rodzaju TYPE_CHECKING and DEBUG. Pozostałe reguły nadal sprawdzają importy tylko dla typów: taki import do zewnętrznej warstwy to nadal INW001. Udział biorą tylko pliki, które sprawdzenie parsuje: te w warstwie, a gdy zadeklarowano konteksty, każdy plik.
Cykl wymaga całego projektu, więc INW004 działa tylko przy sprawdzaniu całego projektu: inwards check bez ścieżek, inwards baseline i bramka Stop z stop-gate = "project". Hook po każdej edycji i edytor sprawdzają jeden plik i nigdy go nie zgłaszają.
Dlaczego to źle¶
Modułów w cyklu nie da się zrozumieć, przetestować ani przenieść pojedynczo; zmiana jednego może zepsuć pozostałe. Python może nawet zawieść przy imporcie, zależnie od tego, który moduł zostanie zaimportowany pierwszy. Agent dodaje import, którego potrzebuje, tam, gdzie go potrzebuje, a do zamknięcia cyklu brakuje jednego importu. Między kontekstami cykl oznacza, że żaden z nich nie jest już niezależny, cokolwiek mówi depends-on.
Przykład¶
Z warstwami z INW001 i włączonymi cyklami modułów:
[tool.inwards]
cycles = ["modules"]
layers = [
{ name = "domain", modules = ["shop.domain"] },
{ name = "application", modules = ["shop.application"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
from shop.domain.discounts import discount
def price(order_id: int) -> int:
return 100 - discount(order_id)
from shop.domain.order import Order
def discount(order_id: int) -> int:
return 0
shop/domain/discounts.py:1:31: INW004 Modules import each other in a cycle: shop.domain.discounts -> shop.domain.order -> shop.domain.pricing -> shop.domain.discounts. The group holds 3 modules and 3 links between them (link hash 2c11a00a0c2cf439).
fix: Break the cycle: one of these modules must stop importing the next.
1. The imports that make the cycle: `from shop.domain.order import Order` in shop.domain.discounts; `from shop.domain.pricing import price` in shop.domain.order; `from shop.domain.discounts import discount` in shop.domain.pricing.
2. Pick the import that points the wrong way, usually from the lower-level module back up, and remove it.
3. Move what both modules need into a third module they can both import, or define a Protocol in the lower-level module and pass the implementation in.
4. If a module needs the other only for type annotations, move that import under `if TYPE_CHECKING:` (before Python 3.14, also quote those annotations or add `from __future__ import annotations`); Inwards doesn't count it, since it never runs. Don't do that for a name the code uses at runtime, and don't move the import into a function: Inwards counts that one.
docs: https://sircypkowskyy.github.io/inwards/rules/INW004/
Po poprawce: discounts nigdy nie potrzebował Order.
Jak naprawić¶
- Przeczytaj ścieżkę: każdy krok to import, a poprawka je wymienia.
- Usuń import, który wskazuje w złą stronę, zwykle z modułu niższego poziomu z powrotem do tego, który go używa.
- Jeśli oba moduły potrzebują tego samego kodu, przenieś go do trzeciego modułu, który oba mogą importować. Jeśli moduł niższego poziomu potrzebuje zachowania z wyższego, daj mu
typing.Protocoli przekaż implementację. - Jeśli jeden moduł potrzebuje drugiego tylko w adnotacjach typów, zaimportuj go pod
if TYPE_CHECKING:. Przed Pythonem 3.14 adnotacje, które go używają, muszą też być napisami albo moduł zaczyna się odfrom __future__ import annotations. Nigdy nie rób tego z nazwą, której kod używa w czasie działania: Python zgłosiNameError, gdy do niej dojdzie. - Między kontekstami zdecyduj, który zależy od którego, i usuń importy idące w drugą stronę. Jeśli
depends-onpozwala na usunięty kierunek, poproś użytkownika o jego usunięcie; hooki nie pozwalają agentowi edytować[tool.inwards].
Bezpieczeństwo poprawki¶
Bez automatycznej poprawki (autofix: false). Który import jest zły, to decyzja projektowa.
Konfiguracja¶
Poziom: błąd. cycles w [tool.inwards] wybiera rodzaje cykli, domyślnie ["contexts"]. Każdy preset inwards init --style zapisuje cycles = ["modules", "contexts"], więc projekt założony przez init dostaje też cykle modułów. [tool.inwards.rules] może regułę wyłączyć albo zmienić w ostrzeżenie (Konfiguracja reguł).
INW004 nie da się wyciszyć w linii: sprawdzenie jednego pliku nie wie, czy wyciszony cykl nadal istnieje, a komentarz siedziałby przy jednym imporcie z wielu. Żeby przyjąć cykle, które kod już ma, uruchom inwards baseline. Nowa grupa i tak nie przejdzie, podobnie jak przyjęta grupa, której powiązania zmienią się w jakikolwiek sposób, bo komunikat podaje ich liczbę i ich skrót. Dotyczy to także grupy, która straciła powiązanie albo element: uruchom inwards baseline ponownie, żeby przyjąć mniejszą grupę.
Znane ograniczenia¶
- Pliki poza wszystkimi warstwami biorą udział dopiero wtedy, gdy zadeklarowano konteksty; wcześniej sprawdzenie ich nie parsuje, więc cykl przez nie nie jest widoczny.
- Import
shop.domain.orderuruchamia teżshop/domain/__init__.py. Ten niejawny krok nie jest krawędzią, więc cykl, który zamyka się tylko przez__init__pakietu, nie jest zgłaszany. - Jeden cykl na grupę: usunięcie go może przy następnym uruchomieniu odsłonić kolejny w tej samej grupie.
- Żadne ustawienie nie przywraca importów spod
TYPE_CHECKINGdo grafu. Cykl, za którym idzie tylko narzędzie do sprawdzania typów, nie wywraca się przy imporcie, a reguły warstw i kontekstów nadal widzą taki import. - Inwards patrzy na nazwę w warunku. Strażnik zapisany inaczej liczy się jak kod wykonywany w czasie działania (
if TC:pofrom typing import TYPE_CHECKING as TC), a projekt, który sam ustawiaTYPE_CHECKING = True, ukryłby prawdziwy cykl.
Źródła¶
- ADR-032: dlaczego cykle są sprawdzane tylko dla całego projektu, jak powstaje graf, dlaczego importy spod
TYPE_CHECKINGnie dają krawędzi i skąd wartość domyślna. - INW002: które konteksty mogą importować które.