failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(None) · None tool calls · 0 s · fastapi/fastapi
✨ Add `iter_route_contexts()` for advanced use cases that used to use `router.routes` (e.g. Jupyverse) ## Pull Request ✨ Add `iter_route_contexts()` for advanced use cases that used to use `router.routes` (e.g. Jupyverse) Related to: https://github.com/fastapi/fastapi/discussions/15782 <!-- Please start with a GitHub Discussion. Once a team member asks you to open a PR, create it and link the discussion here. Obvious typo fixes can be made in a PR without starting a discussion. --> Discussion: <!-- Link to the GitHub Discussion --> ## Description <!-- Write the description of your PR here --> ## AI Disclaimer Codex with GPT 5.5 through several iterations, defining and trimming the interface. Manual review of all the code. <!-- If using AI, write here the prompt and model used --> <details> <summary>AI transcript</summary> <!-- Paste here the entire AI transcript --> </details> ## Checklist - [ ] This PR is an obvious typo fix, or it links to a GitHub Discussion for the proposed code change. - [ ] I added tests for the change. - [ ] The new or updated tests fail on the main branch and pass on this PR. - [ ] Coverage stays at 100%. - [ ] The documentation explains the change if needed.
| # | Tool | Arguments | Result |
|---|---|---|---|
| No trace captured. | |||
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -479,26 +479,22 @@ def get_openapi_path(
def _get_api_route_for_openapi(
- route: BaseRoute, route_context: routing._EffectiveRouteContext | None
+ route_context: routing.RouteContext,
) -> routing._APIRouteLike | None:
- if route_context is not None and isinstance(
- route_context.original_route, routing.APIRoute
- ):
+ if isinstance(route_context.original_route, routing.APIRoute):
return cast(routing._APIRouteLike, route_context)
- if isinstance(route, routing.APIRoute):
- return cast(routing._APIRouteLike, route)
return None
def get_fields_from_routes(
- routes: Sequence[BaseRoute],
+ routes: Sequence[BaseRoute | routing.RouteContext],
) -> list[ModelField]:
body_fields_from_routes: list[ModelField] = []
responses_from_routes: list[ModelField] = []
request_fields_from_routes: list[ModelField] = []
callback_flat_models: list[ModelField] = []
- for route, route_context in routing._iter_routes_with_context(routes):
- api_route = _get_api_route_for_openapi(route, route_context)
+ for route_context in routing.iter_route_contexts(routes):
+ api_route = _get_api_route_for_openapi(route_context)
if api_route is None:
continue
if api_route.include_in_schema:
@@ -531,8 +527,8 @@ def get_openapi(
openapi_version: str = "3.1.0",
summary: str | None = None,
description: str | None = None,
- routes: Sequence[BaseRoute],
- webhooks: Sequence[BaseRoute] | None = None,
+ routes: Sequence[BaseRoute | routing.RouteContext],
+ webhooks: Sequence[BaseRoute | routing.RouteContext] | None = None,
tags: list[dict[str, Any]] | None = None,
servers: list[dict[str, str | Any]] | None = None,
terms_of_service: str | None = None,
@@ -567,8 +563,8 @@ def get_openapi(
model_name_map=model_name_map,
separate_input_output_schemas=separate_input_output_schemas,
)
- for route, route_context in routing._iter_routes_with_context(routes):
- api_route = _get_api_route_for_openapi(route, route_context)
+ for route_context in routing.iter_route_contexts(routes):
+ api_route = _get_api_route_for_openapi(route_context)
if api_route is not None:
result = get_openapi_path(
route=api_route,
@@ -587,8 +583,8 @@ def get_openapi(
)
if path_definitions:
definitions.update(path_definitions)
- for webhook, webhook_context in routing._iter_routes_with_context(webhooks or []):
- api_webhook = _get_api_route_for_openapi(webhook, webhook_context)
+ for webhook_context in routing.iter_route_contexts(webhooks or []):
+ api_webhook = _get_api_route_for_openapi(webhook_context)
if api_webhook is not None:
result = get_openapi_path(
route=api_webhook,
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -1454,6 +1454,47 @@ def url_path_for(self, name: str, /, **path_params: Any) -> Any:
return URLPath(path=path, protocol="http")
+@dataclass(frozen=True)
+class RouteContext:
+ route: BaseRoute
+ _route_context: _EffectiveRouteContext | None = field(default=None, repr=False)
+
+ @property
+ def original_route(self) -> BaseRoute:
+ if self._route_context is not None:
+ return self._route_context.original_route
+ return self.route
+
+ @property
+ def _effective_route(self) -> BaseRoute | _EffectiveRouteContext:
+ if self._route_context is not None:
+ return self._route_context
+ return self.route
+
+ @property
+ def path(self) -> str | None:
+ return getattr(self._effective_route, "path", None)
+
+ @property
+ def path_format(self) -> str | None:
+ return getattr(self._effective_route, "path_format", None)
+
+ @property
+ def name(self) -> str | None:
+ return getattr(self._effective_route, "name", None)
+
+ @property
+ def methods(self) -> set[str] | None:
+ return getattr(self._effective_route, "methods", None)
+
+ @property
+ def endpoint(self) -> Callable[..., Any] | None:
+ return getattr(self._effective_route, "endpoint", None)
+
+ def __getattr__(self, name: str) -> Any:
+ return getattr(self._effective_route, name)
+
+
@dataclass
class _IncludedRouter(BaseRoute):
original_router: "APIRouter"
@@ -1654,6 +1695,20 @@ def _iter_included_route_candidates(routes: Sequence[BaseRoute]) -> Iterator[Bas
yield route
+def iter_route_contexts(
+ routes: Sequence[BaseRoute | RouteContext],
+) -> Iterator[RouteContext]:
+ for route in routes:
+ if isinstance(route, RouteContext):
+ yield route
+ continue
+ for original_route, route_context in _iter_routes_with_context([route]):
+ if route_context is None:
+ yield RouteContext(original_route)
+ else:
+ yield RouteContext(original_route, route_context)
+
+
def _iter_routes_with_context(
routes: Sequence[BaseRoute],
) -> Iterator[tuple[BaseRoute, _EffectiveRouteContext | None]]:...........................F
=================================== FAILURES ===================================
________ test_included_api_route_without_app_scope_returns_405_response ________
async def functions are not natively supported.
You need to install a suitable plugin for your async framework, for example:
- anyio
- pytest-asyncio
- pytest-tornasync
- pytest-trio
- pytest-twisted
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-1.6.0-py3-none-any/starlette/testclient.py:53: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
_PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]
tests/test_router_include_context.py:752
/private/tmp/swe_work/oracle_full/fastapi_15785/b/workspace/tests/test_router_include_context.py:752: PytestUnknownMarkWarning: Unknown pytest.mark.anyio - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.anyio
tests/test_router_include_context.py:866
/private/tmp/swe_work/oracle_full/fastapi_15785/b/workspace/tests/test_router_include_context.py:866: PytestUnknownMarkWarning: Unknown pytest.mark.anyio - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.anyio
tests/test_router_include_context.py:982
/private/tmp/swe_work/oracle_full/fastapi_15785/b/workspace/tests/test_router_include_context.py:982: PytestUnknownMarkWarning: Unknown pytest.mark.anyio - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
@pytest.mark.anyio
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 27 passed, 4 warnings in 0.90s