Skip to content

Commit 79cde91

Browse files
committed
Added routers nesting.
Signed-off-by: Pavel Kirilin <win10@list.ru>
1 parent 43cdb66 commit 79cde91

File tree

4 files changed

+153
-1
lines changed

4 files changed

+153
-1
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,20 @@ web.run_app(app)
5252

5353
```
5454

55+
Also, you can nest routers with prefixes,
56+
57+
```python
58+
api_router = Router()
59+
60+
memes_router = Router()
61+
62+
main_router = Router()
63+
64+
main_router.add_routes(api_router, prefix="/api")
65+
main_router.add_routes(memes_router, prefix="/memes")
66+
```
67+
68+
5569
## Default dependencies
5670

5771
By default this library provides only two injectables. `web.Request` and `web.Application`.

aiohttp_deps/router.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from typing import Iterable
2+
13
from aiohttp import web
24

35

@@ -13,3 +15,25 @@ class Router(web.RouteTableDef):
1315
1416
New types are introduced in stub file: router.pyi.
1517
"""
18+
19+
def add_routes(self, router: Iterable[web.RouteDef], prefix: str = "") -> None:
20+
"""
21+
Append another router's routes to this one.
22+
23+
:param router: router to get routes from.
24+
:param prefix: url prefix for routes, defaults to ""
25+
:raises ValueError: if prefix is incorrect.
26+
"""
27+
if prefix and not prefix.startswith("/"):
28+
raise ValueError("Prefix must start with a `/`")
29+
if prefix and prefix.endswith("/"):
30+
raise ValueError("Prefix should not end with a `/`")
31+
for route in router:
32+
self._items.append(
33+
web.RouteDef(
34+
method=route.method,
35+
path=prefix + route.path,
36+
handler=route.handler,
37+
kwargs=route.kwargs,
38+
),
39+
)

aiohttp_deps/router.pyi

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Awaitable, Callable, Type, Union
1+
from typing import Any, Awaitable, Callable, Iterable, Type, Union
22

33
from aiohttp import web
44
from aiohttp.abc import AbstractView
@@ -18,3 +18,4 @@ class Router(web.RouteTableDef):
1818
def delete(self, path: str, **kwargs: Any) -> _Deco: ...
1919
def options(self, path: str, **kwargs: Any) -> _Deco: ...
2020
def view(self, path: str, **kwargs: Any) -> _Deco: ...
21+
def add_routes(self, router: Iterable[web.RouteDef], prefix: str = "") -> None: ...

tests/test_router.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import pytest
2+
from aiohttp import web
3+
4+
from aiohttp_deps import Router
5+
from tests.conftest import ClientGenerator
6+
7+
8+
@pytest.mark.anyio
9+
async def test_router_add_routes(
10+
my_app: web.Application,
11+
aiohttp_client: ClientGenerator,
12+
):
13+
api_router = Router()
14+
15+
@api_router.get("/a")
16+
async def _():
17+
return web.json_response({"a": "b"})
18+
19+
@api_router.get("/b")
20+
async def _():
21+
return web.json_response({"b": "c"})
22+
23+
router = Router()
24+
router.add_routes(api_router)
25+
26+
my_app.router.add_routes(router)
27+
28+
client = await aiohttp_client(my_app)
29+
response = await client.get("/a")
30+
assert response.status == 200
31+
assert await response.json() == {"a": "b"}
32+
33+
response = await client.get("/b")
34+
assert response.status == 200
35+
assert await response.json() == {"b": "c"}
36+
37+
38+
@pytest.mark.anyio
39+
async def test_prefixed_routes(
40+
my_app: web.Application,
41+
aiohttp_client: ClientGenerator,
42+
):
43+
api_router = Router()
44+
45+
@api_router.get("/a")
46+
async def _():
47+
return web.json_response({"a": "b"})
48+
49+
router = Router()
50+
router.add_routes(api_router, prefix="/api")
51+
52+
my_app.router.add_routes(router)
53+
54+
client = await aiohttp_client(my_app)
55+
response = await client.get("/api/a")
56+
assert response.status == 200
57+
assert await response.json() == {"a": "b"}
58+
59+
60+
@pytest.mark.anyio
61+
async def test_deep_nesting(
62+
my_app: web.Application,
63+
aiohttp_client: ClientGenerator,
64+
):
65+
last_router = Router()
66+
67+
@last_router.get("/a")
68+
async def _():
69+
return web.json_response({"a": "b"})
70+
71+
# Generate 20 nested routers.
72+
for i in range(20):
73+
new_router = Router()
74+
new_router.add_routes(last_router, prefix=f"/{i}")
75+
last_router = new_router
76+
77+
router = Router()
78+
router.add_routes(last_router, prefix="/api")
79+
80+
my_app.router.add_routes(router)
81+
82+
client = await aiohttp_client(my_app)
83+
url = "/api/" + "/".join([str(i) for i in range(20)][::-1]) + "/a"
84+
print(url)
85+
response = await client.get(url)
86+
assert response.status == 200
87+
assert await response.json() == {"a": "b"}
88+
89+
90+
@pytest.mark.anyio
91+
async def test_prefixed_routes_no_start_slash():
92+
api_router = Router()
93+
94+
@api_router.get("/a")
95+
async def _():
96+
"""Nothing."""
97+
98+
router = Router()
99+
with pytest.raises(ValueError):
100+
router.add_routes(api_router, prefix="api")
101+
102+
103+
@pytest.mark.anyio
104+
async def test_prefixed_routes_trailing_slash():
105+
api_router = Router()
106+
107+
@api_router.get("/a")
108+
async def _():
109+
"""Nothing."""
110+
111+
router = Router()
112+
with pytest.raises(ValueError):
113+
router.add_routes(api_router, prefix="/api/")

0 commit comments

Comments
 (0)