Przejdź do treści

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:

touch shop/domain/prices.py shop/domain/money.py
shop/domain/order.py
from shop.domain.pricing import DiscountPolicy
inwards check
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.

shop/domain/prices.py
class DiscountPolicy:
    pass
shop/domain/order.py
from shop.domain.prices import DiscountPolicy
inwards check

Jak naprawić

  1. Porównaj nazwę z najbliższymi modułami z poprawki i zaimportuj ten, który istnieje.
  2. Jeśli kodu jeszcze nie ma, najpierw utwórz moduł, a dopiero potem go importuj.
  3. 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):

pyproject.toml
[tool.inwards]
generated = ["*_pb2", "*_pb2_grpc", "_version", "shop.api.gen"]
  • 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 jak ignore: *_pb2 obejmuje shop.api.orders_pb2, _version obejmuje shop._version, shop.api.gen wszystko w shop/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żyj ignore = ["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, a generated = [] 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:

shop/domain/order.py
from shop.domain.orders_pb2 import Order
inwards check

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 .venv obok pyproject.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-packages w [tool.inwards], dla uruchomień bez środowiska wirtualnego, na przykład w CI przed uv sync. Moduł leżący bezpośrednio w wymienionym pakiecie, którego nie ma w projekcie, liczy się jako istniejący, niezależnie od nazwy.
pyproject.toml
[tool.inwards]
namespace-packages = ["acme.platform"]

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 pricing przechodzi, gdy shop/domain jest pakietem, bo pricing moż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_grpc albo _version (shop.api.payments_pb2 bez payments.proto). Zawiedzie dopiero przy uruchomieniu kodu. Żeby wyłapywać takie importy, ustaw generated = [] i uruchamiaj generator przed sprawdzeniem (ADR-029).
  • Opcjonalny import za try/except ImportError jest zgłaszany.
  • Przeszukiwany jest tylko .venv obok 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 .pth nie są; zamiast tego wpisz pakiet do namespace-packages.
  • namespace-packages przepuszcza 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