Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Route definitions #2004

Merged
merged 31 commits into from
Aug 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a2fbd23
First scratches
asvetlov May 24, 2017
62f653a
Work on
asvetlov Jun 1, 2017
2ce063f
Work on decorators
asvetlov Jun 4, 2017
727ea50
Merge branch 'master' into route_deco
asvetlov Jun 14, 2017
daa31ec
Merge branch 'master' into route_deco
asvetlov Jun 20, 2017
0c23ebf
Make examples work
asvetlov Jun 20, 2017
6cab8b8
Refactor
asvetlov Jun 20, 2017
8b44cde
sort modules for scanning
asvetlov Jun 20, 2017
1d5492c
Go forward
asvetlov Jun 20, 2017
3eb5137
Add tests
asvetlov Jun 20, 2017
a5c24a0
Add test for decoration methods
asvetlov Jun 21, 2017
910cf1c
Add missing file
asvetlov Jun 21, 2017
36d4fc0
Fix python 3.4, add test
asvetlov Jun 22, 2017
de48fda
Fix typo
asvetlov Jun 22, 2017
234ed0e
Implement RouteDef
asvetlov Jun 22, 2017
2f5bb08
Merge branch 'master' into route_deco2
asvetlov Jun 22, 2017
d158513
Test cover
asvetlov Jun 22, 2017
b8ad2f4
RouteDef -> RoutesDef
asvetlov Jun 22, 2017
7391a69
RouteInfo -> RouteDef
asvetlov Jun 22, 2017
341da53
Add couple TODOs, drop RouteDef from exported names
asvetlov Jun 22, 2017
ec0630a
Fix flake8 blame
asvetlov Jun 23, 2017
9c1845c
RoutesDef -> RouteTableDef
asvetlov Aug 2, 2017
a9cbbb4
Merge remote-tracking branch 'origin/master' into route_deco2
asvetlov Aug 2, 2017
e2bc869
Add reprs
asvetlov Aug 2, 2017
58686cb
Add changes record
asvetlov Aug 2, 2017
875ae8e
Test cover missed case
asvetlov Aug 2, 2017
7c54e36
Add documentation for new route definitions API in web reference
asvetlov Aug 2, 2017
48203d3
Fix typo
asvetlov Aug 2, 2017
68f7225
Mention route tables and route decorators in web usage
asvetlov Aug 2, 2017
aaab3ed
Text flow polishing
asvetlov Aug 2, 2017
c10f4e0
Fix typo
asvetlov Aug 3, 2017
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
107 changes: 105 additions & 2 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import os
import re
import warnings
from collections.abc import Container, Iterable, Sized
from collections import namedtuple
from collections.abc import Container, Iterable, Sequence, Sized
from functools import wraps
from pathlib import Path
from types import MappingProxyType
Expand All @@ -28,13 +29,32 @@
__all__ = ('UrlDispatcher', 'UrlMappingMatchInfo',
'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource',
'AbstractRoute', 'ResourceRoute',
'StaticResource', 'View')
'StaticResource', 'View', 'RouteDef', 'RouteTableDef',
'head', 'get', 'post', 'patch', 'put', 'delete', 'route')

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


class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')):
def __repr__(self):
info = []
for name, value in sorted(self.kwargs.items()):
info.append(", {}={!r}".format(name, value))
return ("<RouteDef {method} {path} -> {handler.__name__!r}"
"{info}>".format(method=self.method, path=self.path,
handler=self.handler, info=''.join(info)))

def register(self, router):
if self.method in hdrs.METH_ALL:
reg = getattr(router, 'add_'+self.method.lower())
reg(self.path, self.handler, **self.kwargs)
else:
router.add_route(self.method, self.path, self.handler,
**self.kwargs)


class AbstractResource(Sized, Iterable):

def __init__(self, *, name=None):
Expand Down Expand Up @@ -897,3 +917,86 @@ def freeze(self):
super().freeze()
for resource in self._resources:
resource.freeze()

def add_routes(self, routes):
"""Append routes to route table.

Parameter should be a sequence of RouteDef objects.
"""
# TODO: add_table maybe?
for route in routes:
route.register(self)


def route(method, path, handler, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think after refactoring this function is not required

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is: user might want to provide custom HTTP method in very rare cases

return RouteDef(method, path, handler, kwargs)


def head(path, handler, **kwargs):
return route(hdrs.METH_HEAD, path, handler, **kwargs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code gets more readable when we do not use abbreviations. Can I open a PR by renaming hdrs to headers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good in general but for this particular case it doesn't make sense I think.
hdrs is not a part of our public API anyway.



def get(path, handler, *, name=None, allow_head=True, **kwargs):
return route(hdrs.METH_GET, path, handler, name=name,
allow_head=allow_head, **kwargs)


def post(path, handler, **kwargs):
return route(hdrs.METH_POST, path, handler, **kwargs)


def put(path, handler, **kwargs):
return route(hdrs.METH_PUT, path, handler, **kwargs)


def patch(path, handler, **kwargs):
return route(hdrs.METH_PATCH, path, handler, **kwargs)


def delete(path, handler, **kwargs):
return route(hdrs.METH_DELETE, path, handler, **kwargs)


class RouteTableDef(Sequence):
"""Route definition table"""
def __init__(self):
self._items = []

def __repr__(self):
return "<RouteTableDef count={}>".format(len(self._items))

def __getitem__(self, index):
return self._items[index]

def __iter__(self):
return iter(self._items)

def __len__(self):
return len(self._items)

def __contains__(self, item):
return item in self._items

def route(self, method, path, **kwargs):
def inner(handler):
self._items.append(RouteDef(method, path, handler, kwargs))
return handler
return inner

def head(self, path, **kwargs):
return self.route(hdrs.METH_HEAD, path, **kwargs)

def get(self, path, **kwargs):
return self.route(hdrs.METH_GET, path, **kwargs)

def post(self, path, **kwargs):
return self.route(hdrs.METH_POST, path, **kwargs)

def put(self, path, **kwargs):
return self.route(hdrs.METH_PUT, path, **kwargs)

def patch(self, path, **kwargs):
return self.route(hdrs.METH_PATCH, path, **kwargs)

def delete(self, path, **kwargs):
return self.route(hdrs.METH_DELETE, path, **kwargs)
1 change: 1 addition & 0 deletions changes/2004.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement `router.add_routes` and router decorators.
189 changes: 126 additions & 63 deletions docs/web.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,6 @@ family are plain shortcuts for :meth:`UrlDispatcher.add_route`.
Introduce resources.


.. _aiohttp-web-custom-resource:

Custom resource implementation
------------------------------

To register custom resource use :meth:`UrlDispatcher.register_resource`.
Resource instance must implement `AbstractResource` interface.

.. versionadded:: 1.2.1


.. _aiohttp-web-variable-handler:

Variable Resources
Expand Down Expand Up @@ -331,6 +320,69 @@ viewed using the :meth:`UrlDispatcher.named_resources` method::
:meth:`UrlDispatcher.resources` instead of
:meth:`UrlDispatcher.named_routes` / :meth:`UrlDispatcher.routes`.


Alternative ways for registering routes
---------------------------------------

Code examples shown above use *imperative* style for adding new
routes: they call ``app.router.add_get(...)`` etc.

There are two alternatives: route tables and route decorators.

Route tables look like Django way::

async def handle_get(request):
...


async def handle_post(request):
...

app.router.add_routes([web.get('/get', handle_get),
web.post('/post', handle_post),


The snippet calls :meth:`~aiohttp.web.UrlDispather.add_routes` to
register a list of *route definitions* (:class:`aiohttp.web.RouteDef`
instances) created by :func:`aiohttp.web.get` or
:func:`aiohttp.web.post` functions.

.. seealso:: :ref:`aiohttp-web-route-def` reference.

Route decorators are closer to Flask approach::

routes = web.RouteTableDef()

@routes.get('/get')
async def handle_get(request):
...


@routes.post('/post')
async def handle_post(request):
...

app.router.add_routes(routes)

The example creates a :class:`aiohttp.web.RouteTableDef` container first.

The container is a list-like object with additional decorators
:meth:`aiohttp.web.RouteTableDef.get`,
:meth:`aiohttp.web.RouteTableDef.post` etc. for registering new
routes.

After filling the container
:meth:`~aiohttp.web.UrlDispather.add_routes` is used for adding
registered *route definitions* into application's router.

.. seealso:: :ref:`aiohttp-web-route-table-def` reference.

All tree ways (imperative calls, route tables and decorators) are
equivalent, you could use what do you prefer or even mix them on your
own.

.. versionadded:: 2.3

Custom Routing Criteria
-----------------------

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


.. _aiohttp-web-expect-header:

*Expect* Header
---------------

:mod:`aiohttp.web` supports *Expect* header. By default it sends
``HTTP/1.1 100 Continue`` line to client, or raises
:exc:`HTTPExpectationFailed` if header value is not equal to
"100-continue". It is possible to specify custom *Expect* header
handler on per route basis. This handler gets called if *Expect*
header exist in request after receiving all headers and before
processing application's :ref:`aiohttp-web-middlewares` and
route handler. Handler can return *None*, in that case the request
processing continues as usual. If handler returns an instance of class
:class:`StreamResponse`, *request handler* uses it as response. Also
handler can raise a subclass of :exc:`HTTPException`. In this case all
further processing will not happen and client will receive appropriate
http response.

.. note::
A server that does not understand or is unable to comply with any of the
expectation values in the Expect field of a request MUST respond with
appropriate error status. The server MUST respond with a 417
(Expectation Failed) status if any of the expectations cannot be met or,
if there are other problems with the request, some other 4xx status.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20

If all checks pass, the custom handler *must* write a *HTTP/1.1 100 Continue*
status code before returning.

The following example shows how to setup a custom handler for the *Expect*
header::

async def check_auth(request):
if request.version != aiohttp.HttpVersion11:
return

if request.headers.get('EXPECT') != '100-continue':
raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)

if request.headers.get('AUTHORIZATION') is None:
raise HTTPForbidden()

request.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")

async def hello(request):
return web.Response(body=b"Hello, world")

app = web.Application()
app.router.add_get('/', hello, expect_handler=check_auth)

.. _aiohttp-web-forms:

HTTP Forms
Expand Down Expand Up @@ -1106,6 +1106,69 @@ To manual mode switch :meth:`~StreamResponse.set_tcp_cork` and
be helpful for better streaming control for example.


.. _aiohttp-web-expect-header:

*Expect* Header
---------------

:mod:`aiohttp.web` supports *Expect* header. By default it sends
``HTTP/1.1 100 Continue`` line to client, or raises
:exc:`HTTPExpectationFailed` if header value is not equal to
"100-continue". It is possible to specify custom *Expect* header
handler on per route basis. This handler gets called if *Expect*
header exist in request after receiving all headers and before
processing application's :ref:`aiohttp-web-middlewares` and
route handler. Handler can return *None*, in that case the request
processing continues as usual. If handler returns an instance of class
:class:`StreamResponse`, *request handler* uses it as response. Also
handler can raise a subclass of :exc:`HTTPException`. In this case all
further processing will not happen and client will receive appropriate
http response.

.. note::
A server that does not understand or is unable to comply with any of the
expectation values in the Expect field of a request MUST respond with
appropriate error status. The server MUST respond with a 417
(Expectation Failed) status if any of the expectations cannot be met or,
if there are other problems with the request, some other 4xx status.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20

If all checks pass, the custom handler *must* write a *HTTP/1.1 100 Continue*
status code before returning.

The following example shows how to setup a custom handler for the *Expect*
header::

async def check_auth(request):
if request.version != aiohttp.HttpVersion11:
return

if request.headers.get('EXPECT') != '100-continue':
raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)

if request.headers.get('AUTHORIZATION') is None:
raise HTTPForbidden()

request.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")

async def hello(request):
return web.Response(body=b"Hello, world")

app = web.Application()
app.router.add_get('/', hello, expect_handler=check_auth)

.. _aiohttp-web-custom-resource:

Custom resource implementation
------------------------------

To register custom resource use :meth:`UrlDispatcher.register_resource`.
Resource instance must implement `AbstractResource` interface.

.. versionadded:: 1.2.1


.. _aiohttp-web-graceful-shutdown:

Graceful shutdown
Expand Down
Loading