INW010 unknown-first-party¶
Co robi¶
Zgłasza statyczny import, w pliku w warstwie, własnego modułu, który nie istnieje, oraz import względny, który wychodzi ponad pakiet najwyższego poziomu, czego Python zawsze odmawia. Import jest własny, gdy jego prefiks jest modułem projektu.
Sprawdzana jest tylko część będąca modułem: X w from X import name i cała nazwa w import X albo from X import *. Istnienie jest sondowane na dysku tak, jak importuje Python, więc liczą się pakiety przestrzeni nazw, zaślepki (.pyi) i skompilowane moduły rozszerzeń (.so, .pyd, .pyx, .pyc). Poprawka wymienia trzy moduły z tego samego pakietu najbliższe brakującej nazwie. Pakiety, których __init__.py rozszerza __path__ (pkgutil.extend_path, declare_namespace), są pomijane. Pod niejawnym pakietem przestrzeni nazw (bez __init__.py od pakietu najwyższego poziomu w dół) moduł, który ma inny członek workspace'u uv albo środowisko wirtualne projektu, liczy się jako istniejący, bo Python scala pakiet z każdego katalogu, który go ma (Monorepo i workspace'y uv, zainstalowane pakiety przestrzeni nazw). Moduł objęty wzorcem z generated liczy się jako istniejący, bo zapisuje go krok budowania.
Import zgłoszony przez INW010 albo przepuszczony, bo ma go inna część pakietu przestrzeni nazw, nie dostaje dodatkowo INW006, a import na zewnątrz zgłoszony przez INW001 nie dostaje INW010: poprawka INW001 usuwa import, a poprawka INW010 tworzyłaby moduł.
Dlaczego to źle¶
Agenci wymyślają wiarygodnie brzmiące moduły: from shop.domain.pricing import DiscountPolicy, choć pricing nie istnieje. Bez sprawdzenia wychodzi to jako ImportError dopiero przy testach, o ile jakiś test obejmuje plik. INW010 zgłasza to w tym samym uruchomieniu hooka, zanim powstanie jakikolwiek test, i wskazuje bliską nazwę, o którą agentowi zapewne chodziło.
Przykład¶
Z warstwami z INW001 oraz prices.py i money.py w domenie:
shop/domain/order.py:1:33: INW010 "shop.domain.pricing" is not a module of this project: "shop.domain" has no "pricing".
fix: Import a module that exists, or create "shop.domain.pricing" first.
1. Check the name. The closest modules in "shop.domain": `shop.domain.prices`, `shop.domain.money`.
2. If the code isn't written yet, create "shop.domain.pricing" before importing it.
3. Don't wrap the import in try/except ImportError; Inwards checks those too.
docs: https://sircypkowskyy.github.io/inwards/rules/INW010/
Po poprawce: polityka rabatów mieszka w prices.py.
Jak naprawić¶
- Porównaj nazwę z najbliższymi modułami z poprawki i zaimportuj ten, który istnieje.
- Jeśli kodu jeszcze nie ma, najpierw utwórz moduł, a dopiero potem go importuj.
- Nie owijaj importu w
try/except ImportError: reguła sprawdza go i tam, a kod i tak zawiódłby w czasie działania.
Bezpieczeństwo poprawki¶
Brak automatycznej poprawki (autofix: false). Nazwa najbliższa według odległości edycyjnej nie zawsze jest modułem, o który chodziło w kodzie, więc wybrać musi człowiek albo agent.
Konfiguracja¶
Poziom: błąd. [tool.inwards.rules] może wyłączyć regułę albo zmienić ją w ostrzeżenie (Konfiguracja reguł).
Moduły generowane¶
Niektóre moduły istnieją dopiero po kroku budowania: orders_pb2.py i orders_pb2_grpc.py z protoc, _version.py z setuptools-scm albo hatch-vcs. Checkout programisty je ma, a świeży checkout w CI nie. INW010 traktuje moduł objęty przez generated w [tool.inwards] jako istniejący, niezależnie od tego, czy plik jest (ADR-029):
- Wzorzec to nazwa modułu z kropkami, a każdy segment może używać
*i?. Pasuje do całych segmentów w dowolnym miejscu rozwiązanej nazwy modułu, tak jakignore:*_pb2obejmujeshop.api.orders_pb2,_versionobejmujeshop._version,shop.api.genwszystko wshop/api/gen/. Symbol wieloznaczny nigdy nie przechodzi przez kropkę. - Nie ma zbiorów w nawiasach. Pusty segment, znak, którego nie może być w nazwie modułu (w tym
[i]), albo wzorzec złożony z samych symboli wieloznacznych i kropek (*,*.*) to błąd konfiguracji (kod wyjścia 2). Żeby wyłączyć INW010, użyjignore = ["INW010"]w[tool.inwards.rules]. - Bez tego klucza lista to
["*_pb2", "*_pb2_grpc", "_version"]. Ustawienie klucza zastępuje listę domyślną, więc jeśli nadal chcesz jej wzorców, wypisz je, agenerated = []ją wyłącza. - Klucz czyta tylko INW010. Pozostałe reguły nadal widzą moduł generowany, którego nie ma na dysku, jako brakujący: import takiego modułu skierowany na zewnątrz to nadal INW001, a INW006 wskazuje najbliższy istniejący pakiet.
Przy liście domyślnej import modułu z protoc, który nie został jeszcze wygenerowany, przechodzi:
Każdy inny import można wyciszyć w jego linii, z powodem:
from shop.domain.legacy_rates import RATES # inwards: ignore[INW010] reason="copied in by the deploy script, see #230"
Pakiety przestrzeni nazw współdzielone z zainstalowanymi dystrybucjami¶
Niejawny pakiet przestrzeni nazw (bez __init__.py) może być podzielony między projekt i zainstalowaną dystrybucję. Weźmy lokalny acme/platform/billing/, bez __init__.py w acme/ ani w acme/platform/, i zainstalowaną dystrybucję acme-platform-auth, która dostarcza acme/platform/auth/. import acme.platform.auth.tokens działa przy uruchomieniu kodu, choć auth nie ma w projekcie.
INW010 nie pomija każdego brakującego modułu pod pakietem przestrzeni nazw: zmyślony moduł też by wtedy przeszedł. Potrzebuje dowodu, że moduł leży gdzie indziej:
- Środowisko wirtualne projektu. CLI i hooki zaglądają do site-packages katalogu
.venvobokpyproject.toml, a w workspace'ie uv także w jego katalogu głównym (.venv/lib/python3.*/site-packages, na Windowsie.venv/Lib/site-packages). Moduł, który tam jest, liczy się jako istniejący. Nic nie jest uruchamiane: ani Python, ani sieć. namespace-packagesw[tool.inwards], dla uruchomień bez środowiska wirtualnego, na przykład w CI przeduv sync. Moduł leżący bezpośrednio w wymienionym pakiecie, którego nie ma w projekcie, liczy się jako istniejący, niezależnie od nazwy.
W obu przypadkach pakiet, w którym moduł miałby leżeć, musi być pakietem przestrzeni nazw na całej drodze od pakietu najwyższego poziomu, bo Python scala tylko takie. Brakujący moduł w podpakiecie, który projekt ma, jest nadal zgłaszany: acme.platform.billing.pricing jest błędem, gdy w acme/platform/billing/ nie ma pricing, z kluczem czy bez. Zwykły pakiet (z __init__.py) nie jest scalany, więc brakujący moduł w nim też jest zgłaszany.
Znane ograniczenia¶
from shop.domain import pricingprzechodzi, gdyshop/domainjest pakietem, bopricingmoże być nazwą zdefiniowaną w jego__init__.py(ADR-025).- Zmyślony import, którego nazwę obejmuje wzorzec z
generated, przechodzi INW010, domyślnie każdy moduł*_pb2,*_pb2_grpcalbo_version(shop.api.payments_pb2bezpayments.proto). Zawiedzie dopiero przy uruchomieniu kodu. Żeby wyłapywać takie importy, ustawgenerated = []i uruchamiaj generator przed sprawdzeniem (ADR-029). - Opcjonalny import za
try/except ImportErrorjest zgłaszany. - Przeszukiwany jest tylko
.venvobok configu albo w katalogu głównym workspace'u uv. Środowisko wirtualne gdzie indziej (VIRTUAL_ENV, pamięć podręczna Poetry, conda) i instalacje edytowalne przez pliki.pthnie są; zamiast tego wpisz pakiet donamespace-packages. namespace-packagesprzepuszcza każdą nazwę leżącą bezpośrednio w wymienionym pakiecie, więc zmyślony sąsiad (acme.platform.authz) przechodzi. Głębsze nazwy pod lokalnym podpakietem są nadal sprawdzane.- Importy dynamiczne nie są sprawdzane pod kątem istnienia.
Źródła¶
- ADR-025: INW010 sonduje dysk, żeby ustalić, czy moduł istnieje, i sprawdza tylko część importu będącą modułem
- ADR-029: Moduły generowane przechodzą INW010, domyślnie moduły z protoc i moduły wersji
- Moduły generowane w CI
- Wyłapywanie zmyślonych modułów