INW005 pure-domain¶
Co robi¶
Zgłasza import, w pliku w warstwie, biblioteki, której warstwa nie może używać. Każdy import trafia do jednej z kategorii: własny (należy do warstwy albo projektu; tym zajmują się INW001 i INW006), biblioteka standardowa (dołączona lista nazw modułów CPythona od 3.11 do 3.14) albo biblioteka zewnętrzna. Import biblioteki jest potem sprawdzany względem trzech opcjonalnych kluczy warstwy:
allow-libraries: gdy jest ustawiony, warstwa może importować tylko te biblioteki zewnętrzne. Biblioteka standardowa pozostaje dozwolona.deny-libraries: warstwa nie może importować tych modułów, także z biblioteki standardowej. W najbardziej wewnętrznej warstwie zastępuje domyślną listę zakazów.extend-deny-libraries: dopisuje do listy zakazów warstwy, także do domyślnej.
[tool.inwards.rules.pure-domain] może też zakazać bibliotek prefiksom modułów, które nie są całymi warstwami, kluczem deny (#219): moduły, do których pasuje wpis, nie mogą importować jego bibliotek, w warstwie albo poza wszystkimi warstwami, cokolwiek mówi allow-libraries warstwy. Komunikat podaje wtedy prefiks: Module "mypackage.one.views" imports "django.db" from library "django", which [tool.inwards.rules.pure-domain] denies to "mypackage.one".
Bez żadnego z nich najbardziej wewnętrzna warstwa w konfiguracji z co najmniej dwiema warstwami zakazuje domyślnej listy: frameworków (django, fastapi, flask...), baz danych i ORM-ów (sqlalchemy, psycopg, redis, sqlite3...), klientów sieciowych (requests, httpx, boto3...) i operacji wejścia-wyjścia z biblioteki standardowej (socket, subprocess, http.client, urllib.request...). Pozostałe warstwy mogą importować dowolną bibliotekę. Import jest sprawdzany wszędzie, gdzie się znajduje, jak w INW001, łącznie z dosłownymi importami dynamicznymi (importlib.import_module("requests")). Pełną listę i zasady dopasowania opisuje Biblioteki w warstwach.
Dlaczego to źle¶
INW001 patrzy tylko na twój własny kod, więc from sqlalchemy.orm import Session w domenie ją przechodzi, choć wiąże domenę z bazą danych równie mocno jak import shop.infrastructure.db. Dla agenta, któremu zabroniono importować warstwę infrastruktury, zaimportowanie biblioteki wprost to oczywisty następny ruch. To także najczęstszy wyciek w warstwowym kodzie Pythona pisanym przez ludzi.
Przykład¶
Z warstwami z INW001 domena używa sesji ORM:
from sqlalchemy.orm import Session
class Order:
def __init__(self, session: Session, order_id: int) -> None:
self.session = session
self.order_id = order_id
shop/domain/order.py:1:28: INW005 Layer "domain" imports "sqlalchemy.orm.Session" from library "sqlalchemy", which "domain" may not use.
fix: Use "sqlalchemy" in an outer layer, behind a port owned by "domain".
1. Delete `from sqlalchemy.orm import Session`. Do not move the import into a function, behind TYPE_CHECKING or into importlib; Inwards checks those too.
2. Declare a typing.Protocol in `shop.domain` (for example `shop.domain.ports`) that describes only what this module needs from "sqlalchemy".
3. Type this module against that Protocol and receive the implementation through a constructor or function parameter.
4. Implement the Protocol with "sqlalchemy" in the outer layer that holds adapters (allowed: "application", "infrastructure"), and wire it in the outermost layer (the composition root).
5. If "domain" should be allowed to use "sqlalchemy", ask the user to add "sqlalchemy" to that layer's allow-libraries in [tool.inwards]. Don't edit [tool.inwards] yourself.
docs: https://sircypkowskyy.github.io/inwards/rules/INW005/
Po poprawce domena deklaruje, czego potrzebuje, a infrastruktura implementuje to z użyciem SQLAlchemy.
from typing import Protocol
class Orders(Protocol):
def total(self, order_id: int) -> int: ...
from shop.domain.ports import Orders
def total(orders: Orders, order_id: int) -> int:
return orders.total(order_id)
from sqlalchemy.orm import Session
class SqlOrders:
def __init__(self, session: Session) -> None:
self.session = session
def total(self, order_id: int) -> int:
return 0
Jak naprawić¶
- Usuń import. Przeniesienie go do funkcji, za
TYPE_CHECKINGalbo doimportlibtylko przenosi diagnostykę. - Zadeklaruj w warstwie
typing.Protocol, który opisuje tylko to, czego moduł potrzebuje od biblioteki. - Otypuj moduł względem tego Protocolu i przyjmuj implementację jako parametr.
- Zaimplementuj Protocol z użyciem biblioteki w warstwie zewnętrznej, która może jej używać. Poprawka wymienia wszystkie takie warstwy; wybierz tę z adapterami, która w układzie heksagonalnym nie zawsze jest następną warstwą na zewnątrz.
- Jeśli warstwa powinna móc używać biblioteki, decyduje o tym użytkownik: dopisuje ją do
allow-libraries. Gdy żadna warstwa zewnętrzna też nie może jej używać, poprawka każe agentowi zapytać użytkownika, gdzie biblioteka powinna trafić.
Pakiety workspace'u¶
W workspace (uv) z osobną konfiguracją dla każdego członka pakiet sąsiedniego członka leży poza root konfiguracji, więc INW005 sprawdza go jak bibliotekę. Gdy pakiet najwyższego poziomu importu należy do członka najbliższego [tool.uv.workspace] w katalogu konfiguracji lub wyżej, komunikat mówi "workspace package" zamiast "library": to własny kod zespołu, a nie zależność do usunięcia.
src/qv_datasources/core/x.py:1:8: INW005 Layer "core" imports "qv_core.models" from workspace package "qv_core", which "core" may not use.
fix: "core" may not use workspace package "qv_core": use a package it may use, or ask the user to allow "qv_core".
Poprawka nie każe pisać portu: użyj pakietu albo członka workspace'u, którego warstwa może używać, albo poproś użytkownika o dopisanie pakietu do allow-libraries, które obejmuje też pakiety workspace'u. Pakiety członka to jego [project] name zamieniona na nazwę importu, katalogi w jego src/ oraz, bez src/, katalogi obok jego pyproject.toml zawierające __init__.py. Wzorce członków dopasowują * tylko w obrębie jednego segmentu ścieżki (packages/*); ** nie jest obsługiwane. Rozszerzenie VS Code jeszcze nie szuka workspace'u i nadal mówi "library".
Bezpieczeństwo poprawki¶
Brak automatycznej poprawki (autofix: false). Przeniesienie biblioteki za port zmienia projekt kodu i wymaga przeglądu.
Konfiguracja¶
Poziom: błąd. Dla każdej warstwy, w [tool.inwards]:
[tool.inwards]
layers = [
{ name = "domain", modules = ["shop.domain"], allow-libraries = ["attrs"], extend-deny-libraries = ["os"] },
{ name = "application", modules = ["shop.application"], deny-libraries = ["sqlalchemy", "requests"] },
{ name = "infrastructure", modules = ["shop.infrastructure"] },
]
Żeby zakazać biblioteki części warstwy albo pakietowi, który nie należy do żadnej warstwy:
[tool.inwards.rules.pure-domain]
deny = [{ modules = ["shop.domain.pricing"], libraries = ["numpy"] }]
Wpisy to nazwy importu (yaml, nie PyYAML), które obejmują też podmoduły. Gdy pasują i lista dozwolonych, i lista zakazów, wygrywa dłuższy wpis, a przy remisie allow. deny-libraries = [] w najbardziej wewnętrznej warstwie wyłącza domyślną listę. [tool.inwards.rules] może wyłączyć regułę albo zmienić ją w ostrzeżenie (severity = { INW005 = "warning" }), zobacz Konfiguracja reguł. Jeden import można wyciszyć w jego linii przez # inwards: ignore[INW005] reason="...".
Znane ograniczenia¶
- Nazwy dystrybucji różne od nazwy importu nie są mapowane: wpisz
yaml, niePyYAML. - Lista biblioteki standardowej jest ustalana przy budowaniu, więc moduł dodany w późniejszym CPythonie liczy się jako zewnętrzny, dopóki Inwards nie zaktualizuje listy.
- Włączenie reguły w istniejącej konfiguracji z co najmniej dwiema warstwami może zgłosić biblioteki, których domena używa celowo. Akceptuje je
deny-libraries = [], wpis wallow-librariesalboinwards baseline.
Źródła¶
- ADR-023: Biblioteki w warstwach, z domyślną listą zakazów dla najbardziej wewnętrznej warstwy
- Biblioteki w warstwach