FAPI003 router-wiring¶
Co robi¶
Opt-in. Zgłasza trzy sposoby, w jakie trasy routera FastAPI znikają bez żadnego błędu:
- Niepodpięty router:
APIRouter(...)przypisany do nazwy na poziomie modułu, z co najmniej jedną operacją ścieżki, do którego żadna aplikacja nie dochodzi przezinclude_routeranimount, bezpośrednio ani przez inne routery. Zgłaszany w liniiAPIRouter(...). - Cykl dołączeń: routery, które dołączają się nawzajem, bezpośrednio albo przez inne, albo router, który dołącza sam siebie. Każda grupa routerów jest zgłaszana raz, z całym cyklem w komunikacie, na wywołaniu
include_router, które go zamyka. - Dołączony przed swoimi trasami: w jednym pliku
parent.include_router(child)na poziomie modułu wykonuje się nad dekoratorem@child.get(...).include_routerkopiuje trasy istniejące w chwili wywołania, więc trasy poniżej niego przepadają.
Dlaczego to źle¶
Agent dodaje app/invoices/router.py z trzema endpointami i testuje je na samym routerze, ale nigdy nie dopisuje app.include_router(...) do main.py. Testy przechodzą, a endpointów nie ma. FastAPI nie narzeka na router, którego nikt nie dołącza, a Ruff sprawdza jeden plik naraz, więc nic innego tego nie zgłasza. Cykl albo dołączenie nad trasami daje aplikacji zbiór tras zależny od kolejności wywołań.
Przykład¶
[tool.inwards]
layers = [{ name = "app", modules = ["app"] }]
[tool.inwards.rules]
extend-select = ["FAPI003"]
from fastapi import FastAPI
from app.orders import router as orders
app = FastAPI()
app.include_router(orders.router)
from fastapi import APIRouter
router = APIRouter(prefix="/orders", tags=["orders"])
@router.get("/{order_id}")
async def get_order(order_id: int) -> dict[str, int]:
return {"id": order_id}
from fastapi import APIRouter
router = APIRouter(prefix="/invoices", tags=["invoices"])
@router.get("/{invoice_id}")
async def get_invoice(invoice_id: int) -> dict[str, int]:
return {"id": invoice_id}
app/invoices/router.py:3:10: FAPI003 APIRouter `app.invoices.router.router` has path operations, but no app includes it, directly or through another router, so its routes don't exist at runtime.
fix: Include `router` in the app or router that serves its siblings.
1. In app/main.py, which builds the app `app`, where its sibling routers are included: import `app.invoices.router.router` and add `app.include_router(...)` for it.
2. If it is meant to stay unmounted (a later release, a test-only router), tell the user: they can add it to allow-unmounted in [tool.inwards.rules.router-wiring], or suppress this line with a reason.
3. Don't delete the router or its routes to make this finding go away.
docs: https://sircypkowskyy.github.io/inwards/rules/FAPI003/
Poprawione: main.py dołącza oba routery.
from fastapi import FastAPI
from app.invoices import router as invoices
from app.orders import router as orders
app = FastAPI()
app.include_router(orders.router)
app.include_router(invoices.router)
Jak działa¶
FAPI003 czyta wspólny model FastAPI (ADR-037) i buduje z niego graf aplikacji i routerów:
- Korzenie: każde
FastAPI(...)w projekcie, także zbudowane w funkcji fabrykującej, takiej jakcreate_app(). Zentrypointstylko aplikacje, które ta opcja wymienia. - Krawędzie: każde wywołanie
include_routerimount. Odbiorca i cel są rozwiązywane między plikami przez importy, nigdy przez import aplikacji:router,invoices.routerpofrom app import invoices,invoices_routerpofrom app.invoices.router import router as invoices_router, reeksport w__init__.pyoraz każda nazwa z literału listy lub krotki, na której pętlaforwołainclude_router. - Jeden krok własnego kodu (#391). Gdy odbiorca albo cel jest parametrem funkcji z poziomu modułu, w której stoi wywołanie (
def wire(app, api_router): app.include_router(api_router)), Inwards znajduje każde wywołanie tej funkcji w projekcie (w plikach, które zawierają jej nazwę, także przez reeksporty) i dodaje jedną krawędź na wywołanie, z nazwami, które to wywołanie przekazuje: pozycyjnie, przez słowo kluczowe albo domyślnie. Funkcja użyta inaczej niż przez wywołanie (hooks = [wire]) albo zaimportowana pod inną nazwą (from app.wiring import wire as hook), parametr, który ciało funkcji przypisuje ponownie, wywołanie z*argsalbo**kwargsalbo argument, który nie jest nazwą, zostawia to dołączenie nierozwiązane.include_router(make_router())dołącza router, którymake_routerbuduje i zwraca (router = APIRouter(...)...return router); fabryka, której zwracanego routera nie da się odczytać, zostaje nierozwiązana.mountna parametrze jest śledzony tak samo. - Odbiorcy, których Inwards nie znajduje, na przykład parametr
appfunkcjiregister(app), której nic nie wywołuje: to, co dołączają, liczy się jako osiągnięte, bo coś to dołącza.
Pliki są wybierane filtrem tekstowym (fastapi, APIRouter, include_router...) i parsowane raz. Graf powstaje tylko wtedy, gdy sprawdzany plik zawiera router, aplikację albo wywołanie include_router.
Gdzie działa:
inwards check,inwards baselinei Stop gate zgłaszają wszystkie trzy rodzaje, ale tylko w plikach, które sprawdzają: częściowe sprawdzenie porównuje swoje pliki z grafem całego projektu. Stop gate sprawdza pliki zmienione w sesji, więc router utworzony lub zmieniony w sesji blokuje, jeśli na końcu wciąż nie jest podpięty, a router niepodpięty już na starcie sesji nie blokuje.- Hook PostToolUse i edytor zgłaszają tylko to, co widać w jednym pliku: dołączenie nad trasami i router dołączający sam siebie. Utworzenie routera i podpięcie go w
main.pyto dwie edycje, a blokowanie pierwszej byłoby błędem.
Ten sam graf czytają reguły FastAPI, które potrzebują ścieżki dołączeń (#183, #224, #227): rules/fastapi/model/_graph.ts.
Jak naprawić¶
- Niepodpięty router: dołącz go do aplikacji albo do routera nadrzędnego, w którym dołączane są jego routery siostrzane. Poprawka wskazuje ten moduł. Nie usuwaj routera.
- Cykl: usuń jedno z wywołań
include_router, które wymienia poprawka. Zostaw to, które prowadzi od routera bliższego aplikacji w dół, do jego dziecka. - Dołączony przed swoimi trasami: przenieś wywołanie
include_routerpod ostatni dekorator dołączanego routera albo do modułu, który buduje aplikację.
Bezpieczeństwo poprawki¶
Brak automatycznej poprawki (autofix: false). To, gdzie router powinien być podpięty i które dołączenie w cyklu jest złe, to decyzje projektowe.
Konfiguracja¶
Poziom: błąd. Reguła opt-in: włącza się ją przez extend-select. Jej opcje trafiają do [tool.inwards.rules.router-wiring]:
[tool.inwards.rules]
extend-select = ["FAPI003"]
[tool.inwards.rules.router-wiring]
entrypoints = ["app.main:app"]
allow-unmounted = ["app.experimental"]
unresolved-includes = "warn"
check-order = true
entrypoints(domyślnie: każda aplikacja): aplikacje, od których liczy się podpięcie routerów, w postacimodule:name. Nazwa to zmienna aplikacji albo funkcja najwyższego poziomu, która ją buduje (app.main:create_app). Inne aplikacje, w testach czy skryptach, wtedy się nie liczą. Wpis, który nie wskazuje żadnej aplikacji, nic nie daje.allow-unmounted(domyślnie brak): routery, które mogą zostać niepodpięte, jako prefiksy modułów albo selektory (gramatykamoduleswarstwy) dopasowywane do kwalifikowanej nazwy routera, np.app.experimentalalboapp.*.internal.unresolved-includes(domyślnie"warn"): co robi wywołanieinclude_router, którego celu Inwards nie umie rozwiązać."warn"zamienia każdy niepodpięty router w ostrzeżenie, które wskazuje to wywołanie;"silent"je pomija.check-order(domyślnietrue):falsewyłącza zgłaszanie dołączenia nad trasami.modules, jak w każdej regule (tabele opcji).check-tests:truesprawia, że FAPI003 zgłasza też w modułach testowych, które domyślnie pomija (tabele opcji).
Każda diagnostyka ma własną linię i można ją wyciszyć w linii przez # inwards: ignore[FAPI003] reason="..." w tej linii. Wyciszenie w linii APIRouter(...) liczy się jako użyte także w hooku, który tego routera nie zgłasza.
Znane ograniczenia¶
- Dynamiczne podpinanie:
importlib.import_module(...),getattr(m, "router"), pętla po czymś innym niż literał, więcej niż jeden krok przez funkcje (parametr przekazany dalej do innej funkcji, fabryka wywołująca inną fabrykę), pluginy z entry pointów. Wywołanieinclude_router, którego celu Inwards nie umie rozwiązać, zamienia niepodpięte routery w ostrzeżenia (albo w nic, z"silent"), nigdy w błędy oparte na zgadywaniu. Router biblioteki (from fastapi_users import ...) nie jest nierozwiązanym wywołaniem; po prostu nie ma go w grafie. Router zbudowany przez własną klasę albo fabrykę (#358) nie jest zgłaszany wcale, dopóki takie wywołanie mogłoby dołączyć jego albo router nad nim: wywołanie fabryki, której routera nie da się odczytać, nie może dołączyć żadnego routera znanego Inwards, a każdy inny nierozwiązany cel (parametr, którego wywołań nie da się wszystkich odczytać,getattr) dowolny router. Polar podpina swój router API przez parametradd_versioned_routers(app, api_router), a routery OAuth przez fabryki; zanim #391 zaczęło śledzić te wywołania, wszystkie 72 jego routery były przez to nieznane. Teraz każde dołączenie w Polarze jest śledzone i FAPI003 nie zgłasza żadnego z nich, słusznie: każdy osiąga jakaś aplikacja. - Warunkowe podpinanie:
if settings.DEBUG: app.include_router(debug.router)liczy się jako dołączenie. - Routery celowo nieużywane (wspólny router dołączany przez inne pakiety, router tylko dla testów, router odłożony na kolejne wydanie): użyj
allow-unmountedalbo wyciszenia z powodem. - Aplikacje spoza projektu nie są widoczne: router dołączany przez aplikację z biblioteki jest dla FAPI003 niepodpięty. Dla takiego kodu użyj
allow-unmountedalboignore. - Cykl przez dynamiczne podpinanie nie jest widoczny. Cykl nie zawiesza FastAPI, bo dołączenie kopiuje trasy, ale zbiór tras zależy wtedy od kolejności wywołań, więc jest błędem.
- W Stop gate router, którego sesja nie dotknęła, nie jest zgłaszany, nawet jeśli sesja usunęła jego
include_routerzmain.py.inwards checkgo zgłasza.
Źródła¶
- ADR-037: rodzina FastAPI, opt-in i wspólny model.
- Dokumentacja FastAPI, Bigger Applications:
include_routerna aplikacjach i routerach.