← oracle_full

fastapi_15785

failed WRONG_FIX UNSUBMITTED wrong_fix_unsubmitted(None) · None tool calls · 0 s · fastapi/fastapi

Task input

✨ 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 calls (0)

#ToolArgumentsResult
No trace captured.

Patch

--- 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]]:

Test output

show
...........................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