INW021 private-module¶
Co robi¶
Opt-in, a po włączeniu błąd. Własny moduł, którego nazwa poniżej pakietu najwyższego poziomu zaczyna się od jednego podkreślenia, jest prywatny dla pakietu, który go zawiera: app.users.services._user należy do app.users.services, a wszystko pod app.users._internal należy do app.users. INW021 zgłasza import takiego modułu z każdego modułu spoza tego pakietu. __init__.py pakietu, jego pozostałe moduły i jego podpakiety mogą go importować.
Nazwy dunder (__init__, __main__) nie są prywatne. Sam pakiet najwyższego poziomu nie jest brany pod uwagę, więc _vendor.x jest poza zakresem reguły.
Liczą się te same importy co w INW003:
import app.users.services._user,from app.users.services._user import UserServiceifrom app.users.services import _user(gdy_userjest modułem);- importy względne i z gwiazdką, importy wewnątrz funkcji i importy pod
if TYPE_CHECKING:, które nadal wiążą moduł importujący z prywatną ścieżką; - wywołanie
importlib.import_module("...")z nazwą w literale.
Import, który INW003 zgłasza już w tej samej linii, nie jest zgłaszany drugi raz.
Dlaczego to źle¶
PEP 8 oznacza moduły wewnętrzne, tak jak wewnętrzne funkcje, jednym podkreśleniem na początku nazwy. Pakiet, który trzyma implementację w _user.py i reeksportuje nazwy publiczne z __init__.py, może przenieść, podzielić albo przemianować _user.py bez uprzedzania kogokolwiek. Kod, który importuje _user.py bezpośrednio, łamie tę obietnicę, a następny agent kopiuje ten import.
Reguła Ruffa PLC2701 import-private-name nie obejmuje tego przypadku. Jest w trybie preview i pomija każdy import, którego pakiet najwyższego poziomu jest pakietem pliku importującego, więc w obrębie jednego projektu from app.users.services._user import X w app.orders nigdy nie jest zgłaszany.
Przykład¶
[tool.inwards]
layers = [{ name = "store", modules = ["store"] }]
[tool.inwards.rules]
extend-select = ["INW021"]
class UserService:
def name(self, user_id: int) -> str:
return f"user {user_id}"
"""User services: other packages import what this module re-exports."""
from store.users.services._user import UserService
def greet(user_id: int) -> str:
return UserService().name(user_id)
store/orders/api.py:1:40: INW021 `store.orders.api` imports `store.users.services._user`, a private module of `store.users.services`. A module whose name starts with `_` is for its own package; other code imports what the package's `__init__.py` re-exports.
fix: Import `UserService` from `store.users.services`, not from its private module `store.users.services._user`.
1. In `from store.users.services._user import UserService`, import `UserService` from `store.users.services` instead.
2. If `store.users.services`'s `__init__.py` doesn't re-export it yet, add `from ._user import UserService` there (and to `__all__` if the package keeps one).
3. If `store.users.services._user` is meant for other packages, ask the user whether to rename it without the leading underscore instead.
Po poprawce: pakiet reeksportuje klasę, a moduł wywołujący importuje ją z pakietu.
from store.users.services import UserService
def greet(user_id: int) -> str:
return UserService().name(user_id)
Jak działa¶
INW021 sprawdza każdy moduł, dla którego reguła jest włączona (modules, i nie moduł testowy, chyba że check-tests = true), niezależnie od tego, czy należy do warstwy, więc sprawdza też skrypty. Czyta te same importy co pozostałe reguły importów: linie importów ze wstępnego skanu albo pełne parsowanie, gdy plik go wymaga. Każdy import jest rozwiązywany do własnego modułu, do którego trafia, przez indeks modułów projektu; import modułu, który nie istnieje, należy do INW010, a moduł zewnętrzny nigdy nie jest sprawdzany.
Pakiet, dla którego moduł jest prywatny, to rodzic jego najgłębszego prywatnego segmentu: app._internal.services._user jest prywatny dla app._internal.services, a moduł wewnątrz tego pakietu jest też wewnątrz app.
Jak naprawić¶
- Importuj nazwę z pakietu (
from app.users.services import UserService), nie z jego modułu prywatnego. - Jeśli
__init__.pypakietu jeszcze jej nie reeksportuje, dodaj tamfrom ._user import UserService, a także do__all__, jeśli pakiet je prowadzi. - Jeśli moduł ma służyć innym pakietom, usuń podkreślenie z jego nazwy.
Bezpieczeństwo poprawki¶
Bez automatycznej poprawki (autofix: false). Reeksport nazwy zmienia __init__.py innego pakietu, a zmiana nazwy modułu zmienia każdy jego import.
Konfiguracja¶
Ważność: błąd. Opt-in: włączasz ją przez extend-select. Preset vertical-slices polecenia inwards init włącza INW021 jako ostrzeżenie, bo commands/ i queries/ z jego scaffoldu reeksportują moduły prywatne ze swoich __init__.py. Opcje trafiają do [tool.inwards.rules.private-module] (tabele opcji):
[tool.inwards.rules]
extend-select = ["INW021"]
[tool.inwards.rules.private-module]
modules = ["app"] # optional: only report in these modules
check-tests = true # optional: report in test modules too
modules: jak w każdej regule, moduły, w których INW021 zgłasza, czyli moduły importujące, nie moduły prywatne.check-tests:truesprawia, że INW021 zgłasza też w modułach testowych. Domyślnie je pomija, bo testy często sięgają do modułów, które testują.
Pojedynczy import można wyciszyć z powodem w jego linii: # inwards: ignore[INW021] reason="...".
Znane ograniczenia¶
- Nazwy prywatne nie są objęte.
from app.users.services import _helper, gdy_helperjest funkcją, należy do Ruffa (PLC2701 zgłasza to między pakietami najwyższego poziomu); Inwards nie czyta nazw modułu docelowego. - Prywatne moduły zewnętrzne też należą do Ruffa.
- Moduł prywatny w pakiecie przestrzeni nazw bez
__init__.pynie ma skąd być reeksportowany; dodaj pakietowi__init__.pyalbo zmień nazwę modułu. - Pomiar na korpusie (ADR-046): moduły prywatne ma tylko polar. Dostaje 10 zgłoszeń przy pominiętych testach (8 w kodzie źródłowym, 2 w skryptach), jedno na instrukcję importu. Z
check-tests = truedostaje 67: 57 w testach, wiele z nich to importy prywatnych modułów workera wewnątrz funkcji, między 41 parami modułu importującego i modułu prywatnego. Pozostałe cztery repozytoria nie dostają żadnego.
Źródła¶
- #434: reguła; ADR-046 i #407: decyzja.
- PEP 8: Public and internal interfaces.
- Ruff PLC2701
import-private-namei jej kod źródłowy.