Skip to content

Commit 8f265b8

Browse files
authored
Add convenience for dynamic changes to routing (sanic-org#2704)
1 parent 5ee36fd commit 8f265b8

File tree

2 files changed

+96
-17
lines changed

2 files changed

+96
-17
lines changed

sanic/app.py

+42-17
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from asyncio.futures import Future
1818
from collections import defaultdict, deque
19-
from contextlib import suppress
19+
from contextlib import contextmanager, suppress
2020
from functools import partial
2121
from inspect import isawaitable
2222
from os import environ
@@ -33,6 +33,7 @@
3333
Deque,
3434
Dict,
3535
Iterable,
36+
Iterator,
3637
List,
3738
Optional,
3839
Set,
@@ -433,14 +434,15 @@ def _apply_route(self, route: FutureRoute) -> List[Route]:
433434

434435
ctx = params.pop("route_context")
435436

436-
routes = self.router.add(**params)
437-
if isinstance(routes, Route):
438-
routes = [routes]
437+
with self.amend():
438+
routes = self.router.add(**params)
439+
if isinstance(routes, Route):
440+
routes = [routes]
439441

440-
for r in routes:
441-
r.extra.websocket = websocket
442-
r.extra.static = params.get("static", False)
443-
r.ctx.__dict__.update(ctx)
442+
for r in routes:
443+
r.extra.websocket = websocket
444+
r.extra.static = params.get("static", False)
445+
r.ctx.__dict__.update(ctx)
444446

445447
return routes
446448

@@ -449,17 +451,19 @@ def _apply_middleware(
449451
middleware: FutureMiddleware,
450452
route_names: Optional[List[str]] = None,
451453
):
452-
if route_names:
453-
return self.register_named_middleware(
454-
middleware.middleware, route_names, middleware.attach_to
455-
)
456-
else:
457-
return self.register_middleware(
458-
middleware.middleware, middleware.attach_to
459-
)
454+
with self.amend():
455+
if route_names:
456+
return self.register_named_middleware(
457+
middleware.middleware, route_names, middleware.attach_to
458+
)
459+
else:
460+
return self.register_middleware(
461+
middleware.middleware, middleware.attach_to
462+
)
460463

461464
def _apply_signal(self, signal: FutureSignal) -> Signal:
462-
return self.signal_router.add(*signal)
465+
with self.amend():
466+
return self.signal_router.add(*signal)
463467

464468
def dispatch(
465469
self,
@@ -1520,6 +1524,27 @@ def _check_uvloop_conflict(cls) -> None:
15201524
# Lifecycle
15211525
# -------------------------------------------------------------------- #
15221526

1527+
@contextmanager
1528+
def amend(self) -> Iterator[None]:
1529+
"""
1530+
If the application has started, this function allows changes
1531+
to be made to add routes, middleware, and signals.
1532+
"""
1533+
if not self.state.is_started:
1534+
yield
1535+
else:
1536+
do_router = self.router.finalized
1537+
do_signal_router = self.signal_router.finalized
1538+
if do_router:
1539+
self.router.reset()
1540+
if do_signal_router:
1541+
self.signal_router.reset()
1542+
yield
1543+
if do_signal_router:
1544+
self.signalize(self.config.TOUCHUP)
1545+
if do_router:
1546+
self.finalize()
1547+
15231548
def finalize(self):
15241549
try:
15251550
self.router.finalize()

tests/test_late_adds.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
3+
from sanic import Sanic, text
4+
5+
6+
@pytest.fixture
7+
def late_app(app: Sanic):
8+
app.config.TOUCHUP = False
9+
app.get("/")(lambda _: text(""))
10+
return app
11+
12+
13+
def test_late_route(late_app: Sanic):
14+
@late_app.before_server_start
15+
async def late(app: Sanic):
16+
@app.get("/late")
17+
def handler(_):
18+
return text("late")
19+
20+
_, response = late_app.test_client.get("/late")
21+
assert response.status_code == 200
22+
assert response.text == "late"
23+
24+
25+
def test_late_middleware(late_app: Sanic):
26+
@late_app.get("/late")
27+
def handler(request):
28+
return text(request.ctx.late)
29+
30+
@late_app.before_server_start
31+
async def late(app: Sanic):
32+
@app.on_request
33+
def handler(request):
34+
request.ctx.late = "late"
35+
36+
_, response = late_app.test_client.get("/late")
37+
assert response.status_code == 200
38+
assert response.text == "late"
39+
40+
41+
def test_late_signal(late_app: Sanic):
42+
@late_app.get("/late")
43+
def handler(request):
44+
return text(request.ctx.late)
45+
46+
@late_app.before_server_start
47+
async def late(app: Sanic):
48+
@app.signal("http.lifecycle.request")
49+
def handler(request):
50+
request.ctx.late = "late"
51+
52+
_, response = late_app.test_client.get("/late")
53+
assert response.status_code == 200
54+
assert response.text == "late"

0 commit comments

Comments
 (0)