Skip to content

Commit

Permalink
Support request.url_for when only "app" scope is avaialable (#2672)
Browse files Browse the repository at this point in the history
* Support request.url_for in BaseMiddleware

* Move test to test_requests

* Call the endpoint properly

* fix test

* order the type hint

---------

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
  • Loading branch information
Jdsleppy and Kludex authored Sep 29, 2024
1 parent 1a6018e commit fe46d99
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 9 deletions.
7 changes: 5 additions & 2 deletions starlette/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@


if typing.TYPE_CHECKING:
from starlette.applications import Starlette
from starlette.routing import Router


Expand Down Expand Up @@ -175,8 +176,10 @@ def state(self) -> State:
return self._state

def url_for(self, name: str, /, **path_params: typing.Any) -> URL:
router: Router = self.scope["router"]
url_path = router.url_path_for(name, **path_params)
url_path_provider: Router | Starlette | None = self.scope.get("router") or self.scope.get("app")
if url_path_provider is None:
raise RuntimeError("The `url_for` method can only be used inside a Starlette application or with a router.")
url_path = url_path_provider.url_path_for(name, **path_params)
return url_path.make_absolute_url(base_url=self.base_url)


Expand Down
7 changes: 1 addition & 6 deletions tests/middleware/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

import contextvars
from contextlib import AsyncExitStack
from typing import (
Any,
AsyncGenerator,
AsyncIterator,
Generator,
)
from typing import Any, AsyncGenerator, AsyncIterator, Generator

import anyio
import pytest
Expand Down
43 changes: 42 additions & 1 deletion tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import anyio
import pytest

from starlette.datastructures import Address, State
from starlette.datastructures import URL, Address, State
from starlette.requests import ClientDisconnect, Request
from starlette.responses import JSONResponse, PlainTextResponse, Response
from starlette.types import Message, Receive, Scope, Send
Expand Down Expand Up @@ -592,3 +592,44 @@ async def rcv() -> Message:
assert await s2.__anext__()
with pytest.raises(StopAsyncIteration):
await s1.__anext__()


def test_request_url_outside_starlette_context(test_client_factory: TestClientFactory) -> None:
async def app(scope: Scope, receive: Receive, send: Send) -> None:
request = Request(scope, receive)
request.url_for("index")

client = test_client_factory(app)
with pytest.raises(
RuntimeError,
match="The `url_for` method can only be used inside a Starlette application or with a router.",
):
client.get("/")


def test_request_url_starlette_context(test_client_factory: TestClientFactory) -> None:
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.routing import Route
from starlette.types import ASGIApp

url_for = None

async def homepage(request: Request) -> Response:
return PlainTextResponse("Hello, world!")

class CustomMiddleware:
def __init__(self, app: ASGIApp) -> None:
self.app = app

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
nonlocal url_for
request = Request(scope, receive)
url_for = request.url_for("homepage")
await self.app(scope, receive, send)

app = Starlette(routes=[Route("/home", homepage)], middleware=[Middleware(CustomMiddleware)])

client = test_client_factory(app)
client.get("/home")
assert url_for == URL("http://testserver/home")

0 comments on commit fe46d99

Please sign in to comment.