INW002 context-independence¶
Co robi¶
Zgłasza import z jednego kontekstu do innego, którego kontekst importujący nie wymienia w depends-on. Konteksty deklaruje się w [[tool.inwards.contexts]]; moduł należy do kontekstu, którego prefiks pasuje do najdłuższej części jego nazwy.
- Zadeklarowana zależność jest bezpośrednia. To, że
orderszależy odbilling, pozwalaordersimportowaćbilling, ale nie odwrotnie i nie to, od czego zależybilling. - Kontekst zagnieżdżony w innym jest osobnym kontekstem. Zależność od
billingnie obejmuje kontekstutaxzadeklarowanego wewnątrzshop.billing, a samtaxpotrzebujedepends-on = ["billing"], żeby importować kontekst nadrzędny. - Importy wewnątrz kontekstu i importy, w których któryś koniec nie należy do żadnego kontekstu (wspólne jądro, biblioteka), są pomijane.
- Nie ma tu znaczenia, który z modułów drugiego kontekstu jest importowany: zadeklarowana zależność pozwala na każdy. Pilnowanie, żeby kod z zewnątrz korzystał tylko z modułów
publickontekstu, to zadanie INW003.
Liczy się każdy import, tak jak w INW001: względny, from shop import shipping, import w funkcji albo za TYPE_CHECKING i import dynamiczny, którego cel Inwards potrafi odczytać. Reguła dotyczy każdego pliku, który należy do kontekstu, niezależnie od tego, czy należy też do warstwy. Plik poza wszystkimi warstwami nadal dostaje ostrzeżenie INW006, które wtedy mówi, że jego importy sprawdzają tylko reguły kontekstów. Konteksty i warstwy się sumują, więc jeden import może być jednocześnie INW001 i INW002.
Dlaczego to źle¶
Wycinek, który sięga do kodu innego wycinka, wiąże je ze sobą: zmiana w shipping psuje orders, a żadnego z nich nie da się osobno przenieść, wydzielić ani oddać innemu zespołowi. Agent widzi tylko nazwę, której potrzebuje, znajduje ją w drugim wycinku i importuje, bo to najszybszy sposób na skończenie zadania, a zarazem ten, który rozmywa narysowane przez ciebie granice. depends-on sprawia, że każda dozwolona zależność jest decyzją, którą ktoś zapisał.
Przykład¶
Trzy wycinki w jednej warstwie. Zamówienia mogą korzystać z rozliczeń; z zamówień nie może korzystać nikt, a wysyłka jest osobno:
[tool.inwards]
layers = [
{ name = "slices", modules = ["shop.orders", "shop.billing", "shop.shipping"] },
]
[[tool.inwards.contexts]]
name = "orders"
modules = ["shop.orders"]
depends-on = ["billing"]
[[tool.inwards.contexts]]
name = "billing"
modules = ["shop.billing"]
public = ["shop.billing.invoices"]
[[tool.inwards.contexts]]
name = "shipping"
modules = ["shop.shipping"]
from shop.billing.invoices import issue_invoice
from shop.shipping.labels import print_label
shop/orders/checkout.py:2:34: INW002 Context "orders" imports "shop.shipping.labels.print_label" from context "shipping", which it doesn't declare in depends-on.
fix: Keep "orders" independent of "shipping", or ask the user whether it should depend on it.
1. Delete `from shop.shipping.labels import print_label`. Do not move the import into a function or behind TYPE_CHECKING; Inwards checks those too.
2. Define what "orders" needs as an interface (a Protocol) inside "orders", and let the composition root pass in the "shipping" implementation. "orders" may import from "billing".
3. If "orders" should use "shipping" directly, ask the user to add "shipping" to its depends-on in [tool.inwards]. Don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW002/
Import billing przechodzi: orders go deklaruje. Po poprawce kasa prosi o drukarkę etykiet, zamiast importować tę z wysyłki.
from typing import Protocol
from shop.billing.invoices import issue_invoice
class LabelPrinter(Protocol):
def print_label(self, order_id: int) -> None: ...
def check_out(order_id: int, labels: LabelPrinter) -> None:
issue_invoice(order_id)
labels.print_label(order_id)
Jak naprawić¶
- Usuń import. Przeniesienie go do funkcji, za
TYPE_CHECKINGalbo doimportlibnie pomaga: reguła sprawdza też te miejsca. - Opisz to, czego potrzebuje kontekst importujący, jako jego własny
typing.Protocol, a implementację przyjmij z composition root. INW002 sprawdza też composition root: umieść go w module, który nie należy do żadnego kontekstu (w punkcie wejścia, takim jakshop.main), albo w kontekście, któregodepends-onwymienia oba. Kolejność warstw nadal go obowiązuje, a INW003 ogranicza go do publicznych modułów każdego kontekstu. - Jeśli zależność jest zamierzona, poproś użytkownika o dopisanie drugiego kontekstu do
depends-on. Hooki nie pozwalają agentowi edytować[tool.inwards].
Bezpieczeństwo poprawki¶
Bez automatycznej poprawki (autofix: false). To, czy zależność między dwoma kontekstami jest błędem, czy decyzją, której jeszcze nikt nie zapisał, rozstrzyga użytkownik.
Konfiguracja¶
Poziom: błąd. Reguła czyta [[tool.inwards.contexts]] i bez tej tabeli nic nie robi; depends-on każdego kontekstu wymienia konteksty, które może importować, a dokumentacja konfiguracji opisuje wszystkie klucze. [tool.inwards.rules] może regułę wyłączyć albo zmienić w ostrzeżenie (Konfiguracja reguł).
Pojedynczy import można wyciszyć w jego linii, z podaniem powodu:
from shop.shipping.labels import print_label # inwards: ignore[INW002] reason="until shipping publishes events, #88"
Znane ograniczenia¶
- Kontekst to lista dosłownych prefiksów, więc dwadzieścia wycinków oznacza dwadzieścia tabel. Warstwy przyjmują selektory od #191; konteksty jeszcze nie.
- Nazwa reeksportowana przez moduł, który nie należy do żadnego kontekstu, przechodzi:
from shop.shared import print_labelto importshop.shared, niezależnie od tego, co importuje z koleishop.shared. - Import dynamiczny, którego celu Inwards nie potrafi odczytać, nie jest tu zgłaszany. W warstwie zgłasza go INW011.
Źródła¶
- INW001: ta sama idea dla warstw, do której konteksty się dokładają.
- ADR-030: jak deklaruje się konteksty i dlaczego o przynależności decyduje najdłuższy prefiks.