Skip to content

Commit 8dbad8c

Browse files
authored
Route definitions (#2004)
* First scratches * Work on * Work on decorators * Make examples work * Refactor * sort modules for scanning * Go forward * Add tests * Add test for decoration methods * Add missing file * Fix python 3.4, add test * Fix typo * Implement RouteDef * Test cover * RouteDef -> RoutesDef * RouteInfo -> RouteDef * Add couple TODOs, drop RouteDef from exported names * Fix flake8 blame * RoutesDef -> RouteTableDef * Add reprs * Add changes record * Test cover missed case * Add documentation for new route definitions API in web reference * Fix typo * Mention route tables and route decorators in web usage * Text flow polishing * Fix typo
1 parent e9bf20d commit 8dbad8c

File tree

7 files changed

+833
-66
lines changed

7 files changed

+833
-66
lines changed

aiohttp/web_urldispatcher.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import os
77
import re
88
import warnings
9-
from collections.abc import Container, Iterable, Sized
9+
from collections import namedtuple
10+
from collections.abc import Container, Iterable, Sequence, Sized
1011
from functools import wraps
1112
from pathlib import Path
1213
from types import MappingProxyType
@@ -28,13 +29,32 @@
2829
__all__ = ('UrlDispatcher', 'UrlMappingMatchInfo',
2930
'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource',
3031
'AbstractRoute', 'ResourceRoute',
31-
'StaticResource', 'View')
32+
'StaticResource', 'View', 'RouteDef', 'RouteTableDef',
33+
'head', 'get', 'post', 'patch', 'put', 'delete', 'route')
3234

3335
HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$")
3436
ROUTE_RE = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
3537
PATH_SEP = re.escape('/')
3638

3739

40+
class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')):
41+
def __repr__(self):
42+
info = []
43+
for name, value in sorted(self.kwargs.items()):
44+
info.append(", {}={!r}".format(name, value))
45+
return ("<RouteDef {method} {path} -> {handler.__name__!r}"
46+
"{info}>".format(method=self.method, path=self.path,
47+
handler=self.handler, info=''.join(info)))
48+
49+
def register(self, router):
50+
if self.method in hdrs.METH_ALL:
51+
reg = getattr(router, 'add_'+self.method.lower())
52+
reg(self.path, self.handler, **self.kwargs)
53+
else:
54+
router.add_route(self.method, self.path, self.handler,
55+
**self.kwargs)
56+
57+
3858
class AbstractResource(Sized, Iterable):
3959

4060
def __init__(self, *, name=None):
@@ -897,3 +917,86 @@ def freeze(self):
897917
super().freeze()
898918
for resource in self._resources:
899919
resource.freeze()
920+
921+
def add_routes(self, routes):
922+
"""Append routes to route table.
923+
924+
Parameter should be a sequence of RouteDef objects.
925+
"""
926+
# TODO: add_table maybe?
927+
for route in routes:
928+
route.register(self)
929+
930+
931+
def route(method, path, handler, **kwargs):
932+
return RouteDef(method, path, handler, kwargs)
933+
934+
935+
def head(path, handler, **kwargs):
936+
return route(hdrs.METH_HEAD, path, handler, **kwargs)
937+
938+
939+
def get(path, handler, *, name=None, allow_head=True, **kwargs):
940+
return route(hdrs.METH_GET, path, handler, name=name,
941+
allow_head=allow_head, **kwargs)
942+
943+
944+
def post(path, handler, **kwargs):
945+
return route(hdrs.METH_POST, path, handler, **kwargs)
946+
947+
948+
def put(path, handler, **kwargs):
949+
return route(hdrs.METH_PUT, path, handler, **kwargs)
950+
951+
952+
def patch(path, handler, **kwargs):
953+
return route(hdrs.METH_PATCH, path, handler, **kwargs)
954+
955+
956+
def delete(path, handler, **kwargs):
957+
return route(hdrs.METH_DELETE, path, handler, **kwargs)
958+
959+
960+
class RouteTableDef(Sequence):
961+
"""Route definition table"""
962+
def __init__(self):
963+
self._items = []
964+
965+
def __repr__(self):
966+
return "<RouteTableDef count={}>".format(len(self._items))
967+
968+
def __getitem__(self, index):
969+
return self._items[index]
970+
971+
def __iter__(self):
972+
return iter(self._items)
973+
974+
def __len__(self):
975+
return len(self._items)
976+
977+
def __contains__(self, item):
978+
return item in self._items
979+
980+
def route(self, method, path, **kwargs):
981+
def inner(handler):
982+
self._items.append(RouteDef(method, path, handler, kwargs))
983+
return handler
984+
return inner
985+
986+
def head(self, path, **kwargs):
987+
return self.route(hdrs.METH_HEAD, path, **kwargs)
988+
989+
def get(self, path, **kwargs):
990+
return self.route(hdrs.METH_GET, path, **kwargs)
991+
992+
def post(self, path, **kwargs):
993+
return self.route(hdrs.METH_POST, path, **kwargs)
994+
995+
def put(self, path, **kwargs):
996+
return self.route(hdrs.METH_PUT, path, **kwargs)
997+
998+
def patch(self, path, **kwargs):
999+
return self.route(hdrs.METH_PATCH, path, **kwargs)
1000+
1001+
def delete(self, path, **kwargs):
1002+
return self.route(hdrs.METH_DELETE, path, **kwargs)

changes/2004.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement `router.add_routes` and router decorators.

docs/web.rst

Lines changed: 126 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,6 @@ family are plain shortcuts for :meth:`UrlDispatcher.add_route`.
151151
Introduce resources.
152152

153153

154-
.. _aiohttp-web-custom-resource:
155-
156-
Custom resource implementation
157-
------------------------------
158-
159-
To register custom resource use :meth:`UrlDispatcher.register_resource`.
160-
Resource instance must implement `AbstractResource` interface.
161-
162-
.. versionadded:: 1.2.1
163-
164-
165154
.. _aiohttp-web-variable-handler:
166155

167156
Variable Resources
@@ -331,6 +320,69 @@ viewed using the :meth:`UrlDispatcher.named_resources` method::
331320
:meth:`UrlDispatcher.resources` instead of
332321
:meth:`UrlDispatcher.named_routes` / :meth:`UrlDispatcher.routes`.
333322

323+
324+
Alternative ways for registering routes
325+
---------------------------------------
326+
327+
Code examples shown above use *imperative* style for adding new
328+
routes: they call ``app.router.add_get(...)`` etc.
329+
330+
There are two alternatives: route tables and route decorators.
331+
332+
Route tables look like Django way::
333+
334+
async def handle_get(request):
335+
...
336+
337+
338+
async def handle_post(request):
339+
...
340+
341+
app.router.add_routes([web.get('/get', handle_get),
342+
web.post('/post', handle_post),
343+
344+
345+
The snippet calls :meth:`~aiohttp.web.UrlDispather.add_routes` to
346+
register a list of *route definitions* (:class:`aiohttp.web.RouteDef`
347+
instances) created by :func:`aiohttp.web.get` or
348+
:func:`aiohttp.web.post` functions.
349+
350+
.. seealso:: :ref:`aiohttp-web-route-def` reference.
351+
352+
Route decorators are closer to Flask approach::
353+
354+
routes = web.RouteTableDef()
355+
356+
@routes.get('/get')
357+
async def handle_get(request):
358+
...
359+
360+
361+
@routes.post('/post')
362+
async def handle_post(request):
363+
...
364+
365+
app.router.add_routes(routes)
366+
367+
The example creates a :class:`aiohttp.web.RouteTableDef` container first.
368+
369+
The container is a list-like object with additional decorators
370+
:meth:`aiohttp.web.RouteTableDef.get`,
371+
:meth:`aiohttp.web.RouteTableDef.post` etc. for registering new
372+
routes.
373+
374+
After filling the container
375+
:meth:`~aiohttp.web.UrlDispather.add_routes` is used for adding
376+
registered *route definitions* into application's router.
377+
378+
.. seealso:: :ref:`aiohttp-web-route-table-def` reference.
379+
380+
All tree ways (imperative calls, route tables and decorators) are
381+
equivalent, you could use what do you prefer or even mix them on your
382+
own.
383+
384+
.. versionadded:: 2.3
385+
334386
Custom Routing Criteria
335387
-----------------------
336388

@@ -483,58 +535,6 @@ third-party library, :mod:`aiohttp_session`, that adds *session* support::
483535
web.run_app(make_app())
484536

485537

486-
.. _aiohttp-web-expect-header:
487-
488-
*Expect* Header
489-
---------------
490-
491-
:mod:`aiohttp.web` supports *Expect* header. By default it sends
492-
``HTTP/1.1 100 Continue`` line to client, or raises
493-
:exc:`HTTPExpectationFailed` if header value is not equal to
494-
"100-continue". It is possible to specify custom *Expect* header
495-
handler on per route basis. This handler gets called if *Expect*
496-
header exist in request after receiving all headers and before
497-
processing application's :ref:`aiohttp-web-middlewares` and
498-
route handler. Handler can return *None*, in that case the request
499-
processing continues as usual. If handler returns an instance of class
500-
:class:`StreamResponse`, *request handler* uses it as response. Also
501-
handler can raise a subclass of :exc:`HTTPException`. In this case all
502-
further processing will not happen and client will receive appropriate
503-
http response.
504-
505-
.. note::
506-
A server that does not understand or is unable to comply with any of the
507-
expectation values in the Expect field of a request MUST respond with
508-
appropriate error status. The server MUST respond with a 417
509-
(Expectation Failed) status if any of the expectations cannot be met or,
510-
if there are other problems with the request, some other 4xx status.
511-
512-
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20
513-
514-
If all checks pass, the custom handler *must* write a *HTTP/1.1 100 Continue*
515-
status code before returning.
516-
517-
The following example shows how to setup a custom handler for the *Expect*
518-
header::
519-
520-
async def check_auth(request):
521-
if request.version != aiohttp.HttpVersion11:
522-
return
523-
524-
if request.headers.get('EXPECT') != '100-continue':
525-
raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)
526-
527-
if request.headers.get('AUTHORIZATION') is None:
528-
raise HTTPForbidden()
529-
530-
request.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
531-
532-
async def hello(request):
533-
return web.Response(body=b"Hello, world")
534-
535-
app = web.Application()
536-
app.router.add_get('/', hello, expect_handler=check_auth)
537-
538538
.. _aiohttp-web-forms:
539539

540540
HTTP Forms
@@ -1108,6 +1108,69 @@ To manual mode switch :meth:`~StreamResponse.set_tcp_cork` and
11081108
be helpful for better streaming control for example.
11091109

11101110

1111+
.. _aiohttp-web-expect-header:
1112+
1113+
*Expect* Header
1114+
---------------
1115+
1116+
:mod:`aiohttp.web` supports *Expect* header. By default it sends
1117+
``HTTP/1.1 100 Continue`` line to client, or raises
1118+
:exc:`HTTPExpectationFailed` if header value is not equal to
1119+
"100-continue". It is possible to specify custom *Expect* header
1120+
handler on per route basis. This handler gets called if *Expect*
1121+
header exist in request after receiving all headers and before
1122+
processing application's :ref:`aiohttp-web-middlewares` and
1123+
route handler. Handler can return *None*, in that case the request
1124+
processing continues as usual. If handler returns an instance of class
1125+
:class:`StreamResponse`, *request handler* uses it as response. Also
1126+
handler can raise a subclass of :exc:`HTTPException`. In this case all
1127+
further processing will not happen and client will receive appropriate
1128+
http response.
1129+
1130+
.. note::
1131+
A server that does not understand or is unable to comply with any of the
1132+
expectation values in the Expect field of a request MUST respond with
1133+
appropriate error status. The server MUST respond with a 417
1134+
(Expectation Failed) status if any of the expectations cannot be met or,
1135+
if there are other problems with the request, some other 4xx status.
1136+
1137+
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20
1138+
1139+
If all checks pass, the custom handler *must* write a *HTTP/1.1 100 Continue*
1140+
status code before returning.
1141+
1142+
The following example shows how to setup a custom handler for the *Expect*
1143+
header::
1144+
1145+
async def check_auth(request):
1146+
if request.version != aiohttp.HttpVersion11:
1147+
return
1148+
1149+
if request.headers.get('EXPECT') != '100-continue':
1150+
raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)
1151+
1152+
if request.headers.get('AUTHORIZATION') is None:
1153+
raise HTTPForbidden()
1154+
1155+
request.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
1156+
1157+
async def hello(request):
1158+
return web.Response(body=b"Hello, world")
1159+
1160+
app = web.Application()
1161+
app.router.add_get('/', hello, expect_handler=check_auth)
1162+
1163+
.. _aiohttp-web-custom-resource:
1164+
1165+
Custom resource implementation
1166+
------------------------------
1167+
1168+
To register custom resource use :meth:`UrlDispatcher.register_resource`.
1169+
Resource instance must implement `AbstractResource` interface.
1170+
1171+
.. versionadded:: 1.2.1
1172+
1173+
11111174
.. _aiohttp-web-graceful-shutdown:
11121175

11131176
Graceful shutdown

0 commit comments

Comments
 (0)