Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions starlette/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,8 @@ def debug(self, value: bool) -> None:
self._debug = value
self.middleware_stack = self.build_middleware_stack()

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
return self.router.url_path_for(name, **path_params)
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
return self.router.url_path_for(*args, **path_params)

async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
scope["app"] = self
Expand Down
6 changes: 4 additions & 2 deletions starlette/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,11 @@ def state(self) -> State:
self._state = State(self.scope["state"])
return self._state

def url_for(self, name: str, **path_params: typing.Any) -> str:
def url_for(self, *args: str, **path_params: typing.Any) -> str:
if len(args) != 1:
raise TypeError("url_for() takes exactly one positional argument")
router: Router = self.scope["router"]
url_path = router.url_path_for(name, **path_params)
url_path = router.url_path_for(*args, **path_params)
return url_path.make_absolute_url(base_url=self.base_url)


Expand Down
34 changes: 23 additions & 11 deletions starlette/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

class NoMatchFound(Exception):
"""
Raised by `.url_for(name, **path_params)` and `.url_path_for(name, **path_params)`
Raised by `.url_for(*args, **path_params)` and `.url_path_for(*args, **path_params)`
if no matching route exists.
"""

Expand Down Expand Up @@ -169,7 +169,7 @@ class BaseRoute:
def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
raise NotImplementedError() # pragma: no cover

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
raise NotImplementedError() # pragma: no cover

async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
Expand Down Expand Up @@ -248,7 +248,10 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
return Match.FULL, child_scope
return Match.NONE, {}

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
if len(args) != 1:
raise TypeError("url_path_for() takes exactly one positional argument")
name = args[0]
seen_params = set(path_params.keys())
expected_params = set(self.param_convertors.keys())

Expand Down Expand Up @@ -317,7 +320,10 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
return Match.FULL, child_scope
return Match.NONE, {}

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
if len(args) != 1:
raise TypeError("url_path_for() takes exactly one positional argument")
name = args[0]
seen_params = set(path_params.keys())
expected_params = set(self.param_convertors.keys())

Expand Down Expand Up @@ -390,7 +396,10 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
return Match.FULL, child_scope
return Match.NONE, {}

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
if len(args) != 1:
raise TypeError("url_path_for() takes exactly one positional argument")
name = args[0]
if self.name is not None and name == self.name and "path" in path_params:
# 'name' matches "<mount_name>".
path_params["path"] = path_params["path"].lstrip("/")
Expand Down Expand Up @@ -463,7 +472,10 @@ def matches(self, scope: Scope) -> typing.Tuple[Match, Scope]:
return Match.FULL, child_scope
return Match.NONE, {}

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
if len(args) != 1:
raise TypeError("url_path_for() takes exactly one positional argument")
name = args[0]
if self.name is not None and name == self.name and "path" in path_params:
# 'name' matches "<mount_name>".
path = path_params.pop("path")
Expand Down Expand Up @@ -605,13 +617,13 @@ async def not_found(self, scope: Scope, receive: Receive, send: Send) -> None:
response = PlainTextResponse("Not Found", status_code=404)
await response(scope, receive, send)

def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
def url_path_for(self, *args: str, **path_params: typing.Any) -> URLPath:
for route in self.routes:
try:
return route.url_path_for(name, **path_params)
return route.url_path_for(*args, **path_params)
except NoMatchFound:
pass
raise NoMatchFound(name, path_params)
raise NoMatchFound(args[0], path_params)

async def startup(self) -> None:
"""
Expand Down Expand Up @@ -684,7 +696,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
partial_scope = child_scope

if partial is not None:
#  Handle partial matches. These are cases where an endpoint is
# Handle partial matches. These are cases where an endpoint is
# able to handle the request, but is not a preferred option.
# We use this in particular to deal with "405 Method Not Allowed".
scope.update(partial_scope)
Expand Down Expand Up @@ -712,7 +724,7 @@ def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, Router) and self.routes == other.routes

# The following usages are now discouraged in favour of configuration
#  during Router.__init__(...)
# during Router.__init__(...)
def mount(
self, path: str, app: ASGIApp, name: typing.Optional[str] = None
) -> None: # pragma: nocover
Expand Down
4 changes: 2 additions & 2 deletions starlette/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ def _create_env(
self, directory: typing.Union[str, PathLike], **env_options: typing.Any
) -> "jinja2.Environment":
@pass_context
def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
def url_for(context: dict, *args: str, **path_params: typing.Any) -> str:
request = context["request"]
return request.url_for(name, **path_params)
return request.url_for(*args, **path_params)

loader = jinja2.FileSystemLoader(directory)
env_options.setdefault("loader", loader)
Expand Down
2 changes: 1 addition & 1 deletion starlette/testclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ def __init__(
asgi_app = app
else:
app = typing.cast(ASGI2App, app)
asgi_app = _WrapASGI2(app) #  type: ignore
asgi_app = _WrapASGI2(app) # type: ignore
adapter = _ASGIAdapter(
asgi_app,
portal_factory=self._portal_factory,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import anyio
import pytest

from starlette.applications import Starlette
from starlette.datastructures import Address
from starlette.requests import ClientDisconnect, Request, State
from starlette.responses import JSONResponse, PlainTextResponse, Response
Expand Down Expand Up @@ -420,6 +421,26 @@ async def app(scope, receive, send):
assert result["cookies"] == expected


def test_request_url_for_allows_name_arg(test_client_factory):
app = Starlette()

@app.route("/users/{name}")
async def func_users(request):
raise NotImplementedError() # pragma: no cover

@app.route("/test")
async def func_url_for_test(request: Request):
with pytest.raises(TypeError, match="takes exactly one positional argument"):
request.url_for("func_users", "abcde", "fghij")
url = request.url_for("func_users", name="abcde")
return Response(str(url), media_type="text/plain")

client = test_client_factory(app)
response = client.get("/test")
assert response.status_code == 200
assert response.text == "http://testserver/users/abcde"


def test_chunked_encoding(test_client_factory):
async def app(scope, receive, send):
request = Request(scope, receive)
Expand Down
30 changes: 29 additions & 1 deletion tests/test_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ def user(request):
return Response(content, media_type="text/plain")


def user2(request):
content = "User 2 " + request.path_params["name"]
return Response(content, media_type="text/plain")


def user_me(request):
content = "User fixed me"
return Response(content, media_type="text/plain")
Expand Down Expand Up @@ -114,6 +119,7 @@ async def websocket_params(session: WebSocket):
Route("/", endpoint=users),
Route("/me", endpoint=user_me),
Route("/{username}", endpoint=user),
Route("/n/{name}", endpoint=user2),
Route("/{username}:disable", endpoint=disable_user, methods=["PUT"]),
Route("/nomatch", endpoint=user_no_match),
],
Expand Down Expand Up @@ -186,6 +192,10 @@ def test_router(client):
assert response.status_code == 200
assert response.text == "User tomchristie"

response = client.get("/users/n/tomchristie")
assert response.status_code == 200
assert response.text == "User 2 tomchristie"

response = client.get("/users/me")
assert response.status_code == 200
assert response.text == "User fixed me"
Expand Down Expand Up @@ -253,7 +263,12 @@ def test_route_converters(client):

def test_url_path_for():
assert app.url_path_for("homepage") == "/"
assert app.url_path_for("user", username="tomchristie") == "/users/tomchristie"
assert app.url_path_for("user", username="tomchristie1") == "/users/tomchristie1"
assert app.url_path_for("user2", name="tomchristie2") == "/users/n/tomchristie2"
with pytest.raises(NoMatchFound):
assert app.url_path_for("user", name="tomchristie1")
with pytest.raises(NoMatchFound):
assert app.url_path_for("user2", username="tomchristie2")
assert app.url_path_for("websocket_endpoint") == "/ws"
with pytest.raises(
NoMatchFound, match='No route exists for name "broken" and params "".'
Expand All @@ -267,6 +282,19 @@ def test_url_path_for():
app.url_path_for("user", username="tom/christie")
with pytest.raises(AssertionError):
app.url_path_for("user", username="")
with pytest.raises(TypeError, match="takes exactly one positional argument"):
assert app.url_path_for("user", "args2", name="tomchristie1")
with pytest.raises(TypeError, match="takes exactly one positional argument"):
assert app.url_path_for(name="tomchristie1")
ws_route = WebSocketRoute("/ws", endpoint=websocket_endpoint)
with pytest.raises(TypeError, match="takes exactly one positional argument"):
ws_route.url_path_for("foo", "bar")
mount = Mount("/users", ok, name="users")
with pytest.raises(TypeError, match="takes exactly one positional argument"):
mount.url_path_for("foo", "bar")
host = Host("{subdomain}.example.org", app=subdomain_app, name="subdomains")
with pytest.raises(TypeError, match="takes exactly one positional argument"):
host.url_path_for("foo", "bar")


def test_url_for():
Expand Down