Skip to content
Open
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
17 changes: 12 additions & 5 deletions fasthtml/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -742,12 +742,19 @@ def __repr__(self) -> str:
# %% ../nbs/api/00_core.ipynb #e0accf76
@patch
def add_route(self:FastHTML, route):
"Add or replace a route in the FastHTML app"
"Add or replace a route, rejecting ambiguous method overlaps on the same path and host"
route.methods = [m.upper() for m in listify(route.methods)]
self.router.routes = [r for r in self.router.routes if not
(r.path==route.path and r.name == route.name and
((route.methods is None) or (set(r.methods) == set(route.methods))))]
self.router.routes.append(route)
meths = set(route.methods)
routes = []
for r in self.router.routes:
if (r.path==route.path and r.name == route.name and
((route.methods is None) or (set(r.methods or ()) == meths))): continue
overlap = set(r.methods or ()) & meths
if (r.path == route.path and getattr(r, 'host', None) == getattr(route, 'host', None)
and overlap):
raise ValueError(f"Overlapping route methods {sorted(overlap)!r} for path {route.path!r}")
routes.append(r)
self.router.routes = routes + [route]
self.router.routes.sort(key=lambda r: not getattr(r, 'host', None))


Expand Down
17 changes: 12 additions & 5 deletions nbs/api/00_core.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2108,12 +2108,19 @@
"#| export\n",
"@patch\n",
"def add_route(self:FastHTML, route):\n",
" \"Add or replace a route in the FastHTML app\"\n",
" \"Add or replace a route, rejecting ambiguous method overlaps on the same path and host\"\n",
" route.methods = [m.upper() for m in listify(route.methods)]\n",
" self.router.routes = [r for r in self.router.routes if not\n",
" (r.path==route.path and r.name == route.name and\n",
" ((route.methods is None) or (set(r.methods) == set(route.methods))))]\n",
" self.router.routes.append(route)\n",
" meths = set(route.methods)\n",
" routes = []\n",
" for r in self.router.routes:\n",
" if (r.path==route.path and r.name == route.name and\n",
" ((route.methods is None) or (set(r.methods or ()) == meths))): continue\n",
" overlap = set(r.methods or ()) & meths\n",
" if (r.path == route.path and getattr(r, 'host', None) == getattr(route, 'host', None)\n",
" and overlap):\n",
" raise ValueError(f\"Overlapping route methods {sorted(overlap)!r} for path {route.path!r}\")\n",
" routes.append(r)\n",
" self.router.routes = routes + [route]\n",
" self.router.routes.sort(key=lambda r: not getattr(r, 'host', None))\n"
]
},
Expand Down
34 changes: 34 additions & 0 deletions tests/test_routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import pytest

from fasthtml.core import FastHTML


def _app():
return FastHTML(secret_key="synthetic-test-only", sess_cls=None)


def test_default_route_rejects_overlapping_sibling_method():
app = _app()
app.route("/shared")(lambda req: "default")

with pytest.raises(ValueError, match="POST"):
app.post("/shared")(lambda req: "post")


def test_explicit_disjoint_methods_can_share_path():
app = _app()
app.get("/shared", name="shared_get")(lambda req: "get")
app.post("/shared", name="shared_post")(lambda req: "post")

assert [route.name for route in app.router.routes if route.path == "/shared"] == [
"shared_get",
"shared_post",
]


def test_same_name_and_methods_replace_existing_route():
app = _app()
app.get("/shared", name="shared")(lambda req: "first")
app.get("/shared", name="shared")(lambda req: "replacement")

assert len([route for route in app.router.routes if route.path == "/shared"]) == 1