Przejdź do treści

INW003 public-api-only

Co robi

Zgłasza import, spoza kontekstu, jednego z jego modułów, który nie jest publiczny. Moduł jest publiczny, gdy leży na jednym z prefiksów public kontekstu albo pod nim: public = ["shop.billing.api"] otwiera shop.billing.api i wszystko pod nim, co należy do tego kontekstu, a nic więcej w shop.billing.

  • Wywołujący może należeć do innego kontekstu albo do żadnego, jak punkt wejścia czy skrypt, z warstwą albo bez: gdy konteksty są zadeklarowane, sprawdzane są importy każdego pliku. Tylko kod wewnątrz samego kontekstu może importować jego wnętrze.
  • Dopóki INW002 jest włączona, import z kontekstu, który nie deklaruje celu w depends-on, należy do INW002 i nie dostaje dodatkowo INW003: usunąć trzeba samą zależność, niezależnie od tego, przez który moduł prowadzi. Gdy INW002 jest wyłączona, zgłasza go INW003.
  • Importowana nazwa liczy się jako moduł, w którym żyje: from shop.billing.invoices import issue_invoice importuje shop.billing.invoices.
  • Poprawka wskazuje moduł publiczny, który już udostępnia importowaną nazwę, definiując ją albo importując, jeśli taki istnieje.

Liczy się każdy import, tak jak w INW002: względny, w funkcji albo za TYPE_CHECKING i import dynamiczny, którego cel Inwards potrafi odczytać. Reguła działa niezależnie od tego, czy plik należy do warstwy.

Dlaczego to źle

Moduły publiczne kontekstu to obietnica, którą składa reszcie kodu; wszystko inne może się zmienić bez uprzedzenia. Wywołujący, który importuje moduł wewnętrzny, wiąże się z tym, jak kontekst jest zbudowany dziś, więc zmiana nazwy albo podział wewnątrz billing psuje orders. Agent szuka potrzebnej nazwy i importuje ją stamtąd, gdzie jest zdefiniowana, a to zwykle moduł wewnętrzny. INW003 kieruje go do publicznego.

Przykład

Zamówienia mogą korzystać z rozliczeń, a modułem publicznym rozliczeń jest shop.billing.api, który reeksportuje funkcję wystawiania faktury:

rm -r shop
pyproject.toml
[tool.inwards]
layers = [
  { name = "slices", modules = ["shop.orders", "shop.billing"] },
]

[[tool.inwards.contexts]]
name = "orders"
modules = ["shop.orders"]
depends-on = ["billing"]

[[tool.inwards.contexts]]
name = "billing"
modules = ["shop.billing"]
public = ["shop.billing.api"]
shop/billing/invoices.py
def issue_invoice(order_id: int) -> None:
    pass
shop/billing/api.py
from shop.billing.invoices import issue_invoice

__all__ = ["issue_invoice"]
shop/orders/checkout.py
from shop.billing.invoices import issue_invoice
inwards check
shop/orders/checkout.py:1:35: INW003 Context "orders" imports "shop.billing.invoices.issue_invoice" from context "billing", but "shop.billing.invoices" isn't one of its public modules.
  fix: Import "issue_invoice" from "shop.billing.api", the public module of "billing" that exposes it.
    1. Replace `from shop.billing.invoices import issue_invoice` with `from shop.billing.api import issue_invoice`.
    2. Do not reach the module another way (through a function-level import, TYPE_CHECKING or importlib); Inwards checks those too.
  docs: https://sircypkowskyy.github.io/inwards/rules/INW003/

Po poprawce kasa importuje funkcję z publicznego modułu rozliczeń.

shop/orders/checkout.py
from shop.billing.api import issue_invoice
inwards check

Jak naprawić

  1. Jeśli poprawka wskazuje moduł publiczny, zaimportuj nazwę stamtąd.
  2. W przeciwnym razie reeksportuj to, czego potrzebujesz, z jednego z publicznych modułów kontekstu (from shop.billing.invoices import issue_invoice w shop/billing/api.py), a potem importuj stamtąd. To zmiana kodu, nie konfiguracji.
  3. Jeśli kontekst nie deklaruje modułów publicznych, zapytaj użytkownika, które z jego modułów może importować inny kod. Hooki nie pozwalają agentowi edytować [tool.inwards].

Bezpieczeństwo poprawki

Bez automatycznej poprawki (autofix: false). Moduł publiczny, który udostępnia tę samą nazwę, zwykle udostępnia ten sam obiekt, ale nie musi, a reeksport nazwy z modułu publicznego poszerza obietnicę kontekstu. Jedno i drugie trzeba sprawdzić.

Konfiguracja

Poziom: błąd. Reguła czyta public w [[tool.inwards.contexts]] i bez kontekstów nic nie robi; dokumentacja konfiguracji opisuje wszystkie klucze. Kontekst bez public trzyma wszystkie swoje moduły jako prywatne. [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.billing.invoices import issue_invoice  # inwards: ignore[INW003] reason="api.py lands in #91"

Znane ograniczenia

  • Wpisy public są prefiksami (ADR-030), więc public = ["shop.billing"] otwiera każdy moduł pod pakietem. Żeby fasada pakietu była publiczna bez jego podmodułów _private, zapisz wpis z wiodącym =: public = ["=shop.billing", "shop.billing.models"] otwiera shop.billing oraz shop.billing.models, a from shop.billing._invoices import issue_invoice nadal jest INW003. Inwards nie sprawdza, czy fasada cokolwiek reeksportuje, ani nie traktuje nazw z prefiksem _ w szczególny sposób.
  • Żeby wskazać moduł publiczny, poprawka czyta nazwy, które moduł wiąże na najwyższym poziomie, z jego drzewa składni: def, class, przypisanie z wartością, import pod nazwą z as. Nazwa wymieniona tylko w __all__ albo związana wewnątrz bloku if czy try nie zostanie znaleziona; import i tak jest zgłaszany, z ogólną radą. Serwer języka czyta moduł publiczny ponownie, gdy zostanie zapisany.
  • Import dynamiczny, którego celu Inwards nie potrafi odczytać, nie jest tu zgłaszany. W warstwie zgłasza go INW011.

Źródła

  • INW002: które konteksty mogą importować które; INW003 decyduje, które z ich modułów.
  • ADR-030: jak deklaruje się konteksty i ich moduły publiczne.