Przejdź do treści

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 orders zależy od billing, pozwala orders importować billing, ale nie odwrotnie i nie to, od czego zależy billing.
  • Kontekst zagnieżdżony w innym jest osobnym kontekstem. Zależność od billing nie obejmuje kontekstu tax zadeklarowanego wewnątrz shop.billing, a sam tax potrzebuje depends-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 public kontekstu, 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:

rm -r shop
pyproject.toml
[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"]
shop/billing/invoices.py
def issue_invoice(order_id: int) -> None:
    pass
shop/shipping/labels.py
def print_label(order_id: int) -> None:
    pass
shop/orders/checkout.py
from shop.billing.invoices import issue_invoice
from shop.shipping.labels import print_label
inwards check
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.

shop/orders/checkout.py
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)
inwards check

Jak naprawić

  1. Usuń import. Przeniesienie go do funkcji, za TYPE_CHECKING albo do importlib nie pomaga: reguła sprawdza też te miejsca.
  2. 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 jak shop.main), albo w kontekście, którego depends-on wymienia oba. Kolejność warstw nadal go obowiązuje, a INW003 ogranicza go do publicznych modułów każdego kontekstu.
  3. 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_label to import shop.shared, niezależnie od tego, co importuje z kolei shop.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.