Skip to content

Commit

Permalink
Route definitions (#2004)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
asvetlov authored Aug 4, 2017
1 parent e9bf20d commit 8dbad8c
Show file tree
Hide file tree
Showing 7 changed files with 833 additions and 66 deletions.
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):
return RouteDef(method, path, handler, kwargs)


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


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 @@ -1108,6 +1108,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

0 comments on commit 8dbad8c

Please sign in to comment.