diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 9fc9cc506f..d4e6845118 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -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 @@ -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 (" {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): @@ -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 "".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) diff --git a/changes/2004.feature b/changes/2004.feature new file mode 100644 index 0000000000..b6f51e6403 --- /dev/null +++ b/changes/2004.feature @@ -0,0 +1 @@ +Implement `router.add_routes` and router decorators. diff --git a/docs/web.rst b/docs/web.rst index 3e121c80cb..d0b7384a83 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -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 @@ -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 ----------------------- @@ -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 @@ -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 diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 1a25f0b519..84db0e1b22 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -20,7 +20,7 @@ Servers` (which have no applications, routers, signals and middlewares) and :class:`Request` has an *application* and *match info* attributes. -A :class:`BaseRequest`/:class:`Request` are :obj:`dict`-like objects, +A :class:`BaseRequest` / :class:`Request` are :obj:`dict` like objects, allowing them to be used for :ref:`sharing data` among :ref:`aiohttp-web-middlewares` and :ref:`aiohttp-web-signals` handlers. @@ -1511,6 +1511,15 @@ Router is any object that implements :class:`AbstractRouter` interface. :returns: new :class:`PlainRoute` or :class:`DynamicRoute` instance. + .. method:: add_routes(routes_table) + + Register route definitions from *routes_table*. + + The table is a :class:`list` of :class:`RouteDef` items or + :class:`RouteTableDef`. + + .. versionadded:: 2.3 + .. method:: add_get(path, handler, *, name=None, allow_head=True, **kwargs) Shortcut for adding a GET handler. Calls the :meth:`add_route` with \ @@ -2019,6 +2028,186 @@ and *405 Method Not Allowed*. HTTP status reason +.. _aiohttp-web-route-def: + + +RouteDef +^^^^^^^^ + +Route definition, a description for not registered yet route. + +Could be used for filing route table by providing a list of route +definitions (Django style). + +The definition is created by functions like :func:`get` or +:func:`post`, list of definitions could be added to router by +:meth:`UrlDispatcher.add_routes` call:: + + from aiohttp import web + + async def handle_get(request): + ... + + + async def handle_post(request): + ... + + app.router.add_routes([web.get('/get', handle_get), + web.post('/post', handle_post), + + +.. class:: RouteDef + + A definition for not added yet route. + + .. attribute:: method + + HTTP method (``GET``, ``POST`` etc.) (:class:`str`). + + .. attribute:: path + + Path to resource, e.g. ``/path/to``. Could contain ``{}`` + brackets for :ref:`variable resources + ` (:class:`str`). + + .. attribute:: handler + + An async function to handle HTTP request. + + .. attribute:: kwargs + + A :class:`dict` of additional arguments. + + .. versionadded:: 2.3 + + +.. function:: get(path, handler, *, name=None, allow_head=True, \ + expect_handler=None) + + Return :class:`RouteDef` for processing ``GET`` requests. See + :meth:`UrlDispatcher.add_get` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: post(path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``POST`` requests. See + :meth:`UrlDispatcher.add_post` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: head(path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``HEAD`` requests. See + :meth:`UrlDispatcher.add_head` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: put(path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``PUT`` requests. See + :meth:`UrlDispatcher.add_put` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: patch(path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``PATCH`` requests. See + :meth:`UrlDispatcher.add_patch` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: delete(path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``DELETE`` requests. See + :meth:`UrlDispatcher.add_delete` for information about parameters. + + .. versionadded:: 2.3 + +.. function:: route(method, path, handler, *, name=None, expect_handler=None) + + Return :class:`RouteDef` for processing ``POST`` requests. See + :meth:`UrlDispatcher.add_route` for information about parameters. + + .. versionadded:: 2.3 + +.. _aiohttp-web-route-table-def: + +RouteTableDef +^^^^^^^^^^^^^ + +A routes table definition used for describing routes by decorators +(Flask style):: + + from aiohttp import web + + routes = web.RouteTableDef() + + @routes.get('/get') + async def handle_get(request): + ... + + + @routes.post('/post') + async def handle_post(request): + ... + + app.router.add_routes(routes) + +.. class:: RouteTableDef() + + A sequence of :class:`RouteDef` instances (implements + :class:`abc.collections.Sequence` protocol). + + In addition to all standard :class:`list` methods the class + provides also methods like ``get()`` and ``post()`` for adding new + route definition. + + .. decoratormethod:: get(path, *, allow_head=True, \ + name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``GET`` web-handler. + + See :meth:`UrlDispatcher.add_get` for information about parameters. + + .. decoratormethod:: post(path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``POST`` web-handler. + + See :meth:`UrlDispatcher.add_post` for information about parameters. + + .. decoratormethod:: head(path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``HEAD`` web-handler. + + See :meth:`UrlDispatcher.add_head` for information about parameters. + + .. decoratormethod:: put(path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``PUT`` web-handler. + + See :meth:`UrlDispatcher.add_put` for information about parameters. + + .. decoratormethod:: patch(path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``PATCH`` web-handler. + + See :meth:`UrlDispatcher.add_patch` for information about parameters. + + .. decoratormethod:: delete(path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering ``DELETE`` web-handler. + + See :meth:`UrlDispatcher.add_delete` for information about parameters. + + .. decoratormethod:: route(method, path, *, name=None, expect_handler=None) + + Add a new :class:`RouteDef` item for registering a web-handler + for arbitrary HTTP method. + + See :meth:`UrlDispatcher.add_route` for information about parameters. + + .. versionadded:: 2.3 MatchInfo ^^^^^^^^^ diff --git a/examples/web_srv_route_deco.py b/examples/web_srv_route_deco.py new file mode 100644 index 0000000000..9accbf6e38 --- /dev/null +++ b/examples/web_srv_route_deco.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Example for aiohttp.web basic server +with decorator definition for routes +""" + +import asyncio +import textwrap + +from aiohttp import web + + +routes = web.RouteTableDef() + + +@routes.get('/') +async def intro(request): + txt = textwrap.dedent("""\ + Type {url}/hello/John {url}/simple or {url}/change_body + in browser url bar + """).format(url='127.0.0.1:8080') + binary = txt.encode('utf8') + resp = web.StreamResponse() + resp.content_length = len(binary) + resp.content_type = 'text/plain' + await resp.prepare(request) + resp.write(binary) + return resp + + +@routes.get('/simple') +async def simple(request): + return web.Response(text="Simple answer") + + +@routes.get('/change_body') +async def change_body(request): + resp = web.Response() + resp.body = b"Body changed" + resp.content_type = 'text/plain' + return resp + + +@routes.get('/hello') +async def hello(request): + resp = web.StreamResponse() + name = request.match_info.get('name', 'Anonymous') + answer = ('Hello, ' + name).encode('utf8') + resp.content_length = len(answer) + resp.content_type = 'text/plain' + await resp.prepare(request) + resp.write(answer) + await resp.write_eof() + return resp + + +async def init(): + app = web.Application() + app.router.add_routes(routes) + return app + +loop = asyncio.get_event_loop() +app = loop.run_until_complete(init()) +web.run_app(app) diff --git a/examples/web_srv_route_table.py b/examples/web_srv_route_table.py new file mode 100644 index 0000000000..7d8af62a5c --- /dev/null +++ b/examples/web_srv_route_table.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Example for aiohttp.web basic server +with table definition for routes +""" + +import asyncio +import textwrap + +from aiohttp import web + + +async def intro(request): + txt = textwrap.dedent("""\ + Type {url}/hello/John {url}/simple or {url}/change_body + in browser url bar + """).format(url='127.0.0.1:8080') + binary = txt.encode('utf8') + resp = web.StreamResponse() + resp.content_length = len(binary) + resp.content_type = 'text/plain' + await resp.prepare(request) + resp.write(binary) + return resp + + +async def simple(request): + return web.Response(text="Simple answer") + + +async def change_body(request): + resp = web.Response() + resp.body = b"Body changed" + resp.content_type = 'text/plain' + return resp + + +async def hello(request): + resp = web.StreamResponse() + name = request.match_info.get('name', 'Anonymous') + answer = ('Hello, ' + name).encode('utf8') + resp.content_length = len(answer) + resp.content_type = 'text/plain' + await resp.prepare(request) + resp.write(answer) + await resp.write_eof() + return resp + + +async def init(): + app = web.Application() + app.router.add_routes([ + web.get('/', intro), + web.get('/simple', simple), + web.get('/change_body', change_body), + web.get('/hello/{name}', hello), + web.get('/hello', hello), + ]) + return app + +loop = asyncio.get_event_loop() +app = loop.run_until_complete(init()) +web.run_app(app) diff --git a/tests/test_route_def.py b/tests/test_route_def.py new file mode 100644 index 0000000000..730f73da6a --- /dev/null +++ b/tests/test_route_def.py @@ -0,0 +1,286 @@ +import asyncio + +import pytest + +from aiohttp import web +from aiohttp.web_urldispatcher import UrlDispatcher + + +@pytest.fixture +def router(): + return UrlDispatcher() + + +def test_get(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.get('/', handler)]) + assert len(router.routes()) == 2 # GET and HEAD + + route = list(router.routes())[1] + assert route.handler is handler + assert route.method == 'GET' + assert str(route.url_for()) == '/' + + route2 = list(router.routes())[0] + assert route2.handler is handler + assert route2.method == 'HEAD' + + +def test_head(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.head('/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'HEAD' + assert str(route.url_for()) == '/' + + +def test_post(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.post('/', handler)]) + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'POST' + assert str(route.url_for()) == '/' + + +def test_put(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.put('/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'PUT' + assert str(route.url_for()) == '/' + + +def test_patch(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.patch('/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'PATCH' + assert str(route.url_for()) == '/' + + +def test_delete(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.delete('/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'DELETE' + assert str(route.url_for()) == '/' + + +def test_route(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.route('OTHER', '/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'OTHER' + assert str(route.url_for()) == '/' + + +def test_head_deco(router): + routes = web.RouteTableDef() + + @routes.head('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'HEAD' + assert str(route.url_for()) == '/path' + + +def test_get_deco(router): + routes = web.RouteTableDef() + + @routes.get('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 2 + + route1 = list(router.routes())[0] + assert route1.method == 'HEAD' + assert str(route1.url_for()) == '/path' + + route2 = list(router.routes())[1] + assert route2.method == 'GET' + assert str(route2.url_for()) == '/path' + + +def test_post_deco(router): + routes = web.RouteTableDef() + + @routes.post('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'POST' + assert str(route.url_for()) == '/path' + + +def test_put_deco(router): + routes = web.RouteTableDef() + + @routes.put('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'PUT' + assert str(route.url_for()) == '/path' + + +def test_patch_deco(router): + routes = web.RouteTableDef() + + @routes.patch('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'PATCH' + assert str(route.url_for()) == '/path' + + +def test_delete_deco(router): + routes = web.RouteTableDef() + + @routes.delete('/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'DELETE' + assert str(route.url_for()) == '/path' + + +def test_route_deco(router): + routes = web.RouteTableDef() + + @routes.route('OTHER', '/path') + @asyncio.coroutine + def handler(request): + pass + + router.add_routes(routes) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'OTHER' + assert str(route.url_for()) == '/path' + + +def test_routedef_sequence_protocol(): + routes = web.RouteTableDef() + + @routes.delete('/path') + @asyncio.coroutine + def handler(request): + pass + + assert len(routes) == 1 + + info = routes[0] + assert isinstance(info, web.RouteDef) + assert info in routes + assert list(routes)[0] is info + + +def test_repr_route_def(): + routes = web.RouteTableDef() + + @routes.get('/path') + @asyncio.coroutine + def handler(request): + pass + + rd = routes[0] + assert repr(rd) == " 'handler'>" + + +def test_repr_route_def_with_extra_info(): + routes = web.RouteTableDef() + + @routes.get('/path', extra='info') + @asyncio.coroutine + def handler(request): + pass + + rd = routes[0] + assert repr(rd) == " 'handler', extra='info'>" + + +def test_repr_route_table_def(): + routes = web.RouteTableDef() + + @routes.get('/path') + @asyncio.coroutine + def handler(request): + pass + + assert repr(routes) == ""