Repository: encode/starlette

Issue:
Inconsistent Typing Between request.url_for and application.url_path_for
[`request.url_for` takes `Any` for the `path_params`](https://github.com/encode/starlette/blob/217019b36303ea16a5869ad076d835e4fdc1cef3/starlette/requests.py#L168) whereas [`application.url_path_for` expects `path_params` to be `str`](https://github.com/encode/starlette/blob/537ab6afd110b79dca95f3c8ecc6980710b1de1c/starlette/applications.py#L107). Likewise the route / routers expect `path_params` to be `str`. See [here](https://github.com/encode/starlette/blob/537ab6afd110b79dca95f3c8ecc6980710b1de1c/starlette/routing.py#L159) and [here](https://github.com/encode/starlette/blob/537ab6afd110b79dca95f3c8ecc6980710b1de1c/starlette/routing.py#L584).

The type mismatch is not caught by the type checker as the `router` here is untyped:
https://github.com/encode/starlette/blob/217019b36303ea16a5869ad076d835e4fdc1cef3/starlette/requests.py#L169-L170

I think `Any` is probably more close to being the correct type as the `path_params` can be `UUID`, `int`, etc.

[The docs](https://www.starlette.io/routing/#reverse-url-lookups) would make it seems that the API for these two methods should have a consistent signature.


Discussion:

Resolving patch:
diff --git a/starlette/applications.py b/starlette/applications.py
--- a/starlette/applications.py
+++ b/starlette/applications.py
@@ -104,7 +104,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         return self.router.url_path_for(name, **path_params)
 
     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
diff --git a/starlette/requests.py b/starlette/requests.py
--- a/starlette/requests.py
+++ b/starlette/requests.py
@@ -15,6 +15,10 @@
     parse_options_header = None
 
 
+if typing.TYPE_CHECKING:
+    from starlette.routing import Router
+
+
 SERVER_PUSH_HEADERS_TO_COPY = {
     "accept",
     "accept-encoding",
@@ -166,7 +170,7 @@ def state(self) -> State:
         return self._state
 
     def url_for(self, name: str, **path_params: typing.Any) -> str:
-        router = self.scope["router"]
+        router: Router = self.scope["router"]
         url_path = router.url_path_for(name, **path_params)
         return url_path.make_absolute_url(base_url=self.base_url)
 
diff --git a/starlette/routing.py b/starlette/routing.py
--- a/starlette/routing.py
+++ b/starlette/routing.py
@@ -156,7 +156,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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         raise NotImplementedError()  # pragma: no cover
 
     async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
@@ -235,7 +235,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         seen_params = set(path_params.keys())
         expected_params = set(self.param_convertors.keys())
 
@@ -298,7 +298,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         seen_params = set(path_params.keys())
         expected_params = set(self.param_convertors.keys())
 
@@ -371,7 +371,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         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("/")
@@ -441,7 +441,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         if self.name is not None and name == self.name and "path" in path_params:
             # 'name' matches "<mount_name>".
             path = path_params.pop("path")
@@ -581,7 +581,7 @@ 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: str) -> URLPath:
+    def url_path_for(self, name: str, **path_params: typing.Any) -> URLPath:
         for route in self.routes:
             try:
                 return route.url_path_for(name, **path_params)


Test patch: