Skip to content

Commit 76560bb

Browse files
authored
Merge pull request #25 from volfpeter/feat/url-for-support
Feat: url_for() support
2 parents 18d3ce0 + ff8c012 commit 76560bb

File tree

7 files changed

+102
-4
lines changed

7 files changed

+102
-4
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ uv.lock
2121
# Build
2222

2323
dist/
24+
25+
dev/

docs/file-system-based-routing.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,17 @@ In the example, `my_app/navbar.py` is not a special file, so it is not routable.
8888
For any `page.py` file, you can also define a `handle_submit` function. This automatically creates a `POST` route at the same URL as the page. This is a convenient pattern for handling HTML form submissions that modify data.
8989

9090
If `my_app/users/page.py` defines a `handle_submit` function, it will handle `POST /users` requests, and the `page` function in the same file will handle `GET /users` requests as usual.
91+
92+
## Constructing URLs
93+
94+
FastAPI provides two ways for constructing valid URLs for registered routes: `FastAPI.url_path_for()` and `Request.url_for()`. Both of these methods expect the path operation's name, and the route's path parameters (if any) as keyword arguments.
95+
96+
To let you use these built-in FastAPI utilities, `holm` automatically assigns a name to every page route it registers. The name assignment logic is very simple: the name (import path) of the corresponding `page.py` module.
97+
98+
Here are some examples:
99+
100+
- `app.url_path_for("my_app.page")`: `/`
101+
- `app.url_path_for("my_app.users.page")`: `/users/`
102+
- `app.url_path_for("my_app.users._user_id_.page", user_id=1)`: `/users/1/`
103+
104+
Submit handlers are also assigned a name: the name of the corresponding page's name, followed by the `.handle_submit` suffix. Since the only difference between pages and submit handlers is the used HTTP method, the URL for a submit handler is the same as the URL for the corresponding page.

holm/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.4.0"
1+
__version__ = "0.4.1"
22

33
from .app import App as App
44
from .fastapi import FastAPIDependency as FastAPIDependency

holm/app.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ def _build_api(
103103
api.get(
104104
"/",
105105
response_model=None,
106-
name="page",
106+
# mypy can't infer that the modules is not None.
107+
name=page_module.__name__, # type: ignore[union-attr]
107108
description=page_dep.__doc__,
108109
tags=["Page"],
109110
)(htmy.page(components_with_metadata)(path_operation))
@@ -119,7 +120,8 @@ def _build_api(
119120
api.post(
120121
"/",
121122
response_model=None,
122-
name="submit",
123+
# mypy can't infer that the modules is not None.
124+
name=f"{page_module.__name__}.handle_submit", # type: ignore[union-attr]
123125
description=submit_handler_dep.__doc__,
124126
tags=["Page", "Submit"],
125127
)(htmy.page(components_with_metadata)(path_operation))

holm/modules/_page.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ class PageDefinition(Protocol):
1717
"""Protocol definition for objects (usually modules) that define a page."""
1818

1919
@property
20-
def page(self) -> Page: ...
20+
def __name__(self) -> str:
21+
"""
22+
Page definitions are expected to have a `__name__` attribute,
23+
since they are modules.
24+
"""
25+
...
26+
27+
@property
28+
def page(self) -> Page:
29+
"""The page implementation."""
30+
...
2131

2232

2333
def is_page_definition(obj: Any) -> TypeGuard[PageDefinition]:

test_app/api.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from fastapi import APIRouter, Request
2+
3+
api = APIRouter()
4+
5+
6+
@api.get("/urls")
7+
def list_urls(request: Request) -> list[str]:
8+
"""Lists some urls to demonstrate `request.url_for()` usage."""
9+
return [
10+
str(request.url_for("test_app.page")),
11+
str(request.url_for("test_app.calculator.page")),
12+
str(request.url_for("test_app.user.page")),
13+
str(request.url_for("test_app.user._id_.page", id="1")),
14+
]

tests/test_url_for.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import Any
2+
3+
import pytest
4+
from fastapi.testclient import TestClient
5+
6+
import test_app.calculator.page as calculator_page
7+
import test_app.page as test_app_page
8+
import test_app.user._id_.page as user_by_id_page
9+
import test_app.user.page as user_page
10+
from test_app.main import app
11+
12+
13+
@pytest.mark.parametrize(
14+
("path_name", "url", "path_params"),
15+
(
16+
("test_app.page", "/", {}),
17+
(test_app_page.__name__, "/", {}),
18+
("test_app.calculator.page", "/calculator/", {}),
19+
(calculator_page.__name__, "/calculator/", {}),
20+
("test_app.user.page", "/user/", {}),
21+
(user_page.__name__, "/user/", {}),
22+
("test_app.user._id_.page", "/user/1/", {"id": 1}),
23+
(user_by_id_page.__name__, "/user/1/", {"id": 1}),
24+
("test_app.user._id_.page", "/user/C0FF33/", {"id": "C0FF33"}),
25+
(user_by_id_page.__name__, "/user/C0FF33/", {"id": "C0FF33"}),
26+
),
27+
)
28+
def test_app_url_path_for_page(path_name: str, url: str, path_params: dict[str, Any]) -> None:
29+
"""
30+
Tests that `app.url_path_for()` can construct URLs for pages
31+
based on page module name (import path).
32+
"""
33+
assert app.url_path_for(path_name, **path_params) == url
34+
35+
36+
@pytest.mark.parametrize(
37+
("path_name", "url"),
38+
(("test_app.calculator.page.handle_submit", "/calculator/"),),
39+
)
40+
def test_app_url_path_for_handle_submit(path_name: str, url: str) -> None:
41+
"""
42+
Tests that `app.url_path_for()` can construct URLs for submit handlers
43+
based on page module name (import path) with the `.handle_submit` suffix.
44+
"""
45+
assert app.url_path_for(path_name) == url
46+
47+
48+
def test_request_url_for(client: TestClient) -> None:
49+
response = client.get("/urls")
50+
assert response.status_code == 200
51+
assert response.json() == [
52+
"http://testserver/",
53+
"http://testserver/calculator/",
54+
"http://testserver/user/",
55+
"http://testserver/user/1/",
56+
]

0 commit comments

Comments
 (0)