From a2fbd23d75e3af7b5934c19de2c65fdff3f1afc7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 23 May 2017 19:25:25 -0700 Subject: [PATCH 01/27] First scratches --- aiohttp/web_urldispatcher.py | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 04c4b11795..5cc12ba408 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -1,6 +1,7 @@ import abc import asyncio import collections +import functools import inspect import keyword import os @@ -893,3 +894,56 @@ def freeze(self): super().freeze() for resource in self._resources: resource.freeze() + + +def _reg_http_method(func, method, **kwargs): + if not hasattr(func, '__aiohttp_data__'): + func.__aiohttp_data__ = {} + func.__aiohttp_data__[method] = kwargs + return func + + +def head(**kwargs): + def wrapper(func): + @functools.wraps(func) + def wrapped(**kwargs): + return _reg_http_method(func, hdrs.METH_HEAD, **kwargs) + return wrapped + return wrapper + + +def add_get(self, *args, name=None, allow_head=True, **kwargs): + """ + Shortcut for add_route with method GET, if allow_head is true another + route is added allowing head requests to the same endpoint + """ + if allow_head: + # it name is not None append -head to avoid it conflicting with + # the GET route below + head_name = name and '{}-head'.format(name) + self.add_route(hdrs.METH_HEAD, *args, name=head_name, **kwargs) + return self.add_route(hdrs.METH_GET, *args, name=name, **kwargs) + +def add_post(self, *args, **kwargs): + """ + Shortcut for add_route with method POST + """ + return self.add_route(hdrs.METH_POST, *args, **kwargs) + +def add_put(self, *args, **kwargs): + """ + Shortcut for add_route with method PUT + """ + return self.add_route(hdrs.METH_PUT, *args, **kwargs) + +def add_patch(self, *args, **kwargs): + """ + Shortcut for add_route with method PATCH + """ + return self.add_route(hdrs.METH_PATCH, *args, **kwargs) + +def add_delete(self, *args, **kwargs): + """ + Shortcut for add_route with method DELETE + """ + return self.add_route(hdrs.METH_DELETE, *args, **kwargs) From 62f653a6d8cbe8370d765b5c50038b3545c25734 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 1 Jun 2017 21:53:24 +0300 Subject: [PATCH 02/27] Work on --- aiohttp/web_urldispatcher.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 5cc12ba408..25e3505968 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -897,9 +897,9 @@ def freeze(self): def _reg_http_method(func, method, **kwargs): - if not hasattr(func, '__aiohttp_data__'): - func.__aiohttp_data__ = {} - func.__aiohttp_data__[method] = kwargs + if not hasattr(func, '__aiohttp_web__'): + func.__aiohttp_web__ = {} + func.__aiohttp_web__[method] = kwargs return func @@ -912,7 +912,7 @@ def wrapped(**kwargs): return wrapper -def add_get(self, *args, name=None, allow_head=True, **kwargs): +def get(self, *args, name=None, allow_head=True, **kwargs): """ Shortcut for add_route with method GET, if allow_head is true another route is added allowing head requests to the same endpoint @@ -924,24 +924,28 @@ def add_get(self, *args, name=None, allow_head=True, **kwargs): self.add_route(hdrs.METH_HEAD, *args, name=head_name, **kwargs) return self.add_route(hdrs.METH_GET, *args, name=name, **kwargs) + def add_post(self, *args, **kwargs): """ Shortcut for add_route with method POST """ return self.add_route(hdrs.METH_POST, *args, **kwargs) + def add_put(self, *args, **kwargs): """ Shortcut for add_route with method PUT """ return self.add_route(hdrs.METH_PUT, *args, **kwargs) + def add_patch(self, *args, **kwargs): """ Shortcut for add_route with method PATCH """ return self.add_route(hdrs.METH_PATCH, *args, **kwargs) + def add_delete(self, *args, **kwargs): """ Shortcut for add_route with method DELETE From 2ce063fec8cf5e3bca23c2e78fd203502b9cddd3 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 4 Jun 2017 20:15:27 +0300 Subject: [PATCH 03/27] Work on decorators --- aiohttp/web_urldispatcher.py | 102 +++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 25e3505968..f1497d613b 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -7,7 +7,7 @@ import os import re import warnings -from collections.abc import Container, Iterable, Sized +from collections.abc import Container, Iterable, Sized, namedtuple from pathlib import Path from types import MappingProxyType @@ -33,6 +33,8 @@ HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") PATH_SEP = re.escape('/') +RouteInfo = namedtuple('RouteInfo', 'method, path, handler, kwargs') + class AbstractResource(Sized, Iterable): @@ -895,59 +897,77 @@ def freeze(self): for resource in self._resources: resource.freeze() + def add_routes(self, routes): + for route in routes: + assert route.method in hdrs.METH_ALL + reg = getattr(self, 'add_'+route.method.lower()) + reg(route.path, route.handler, **route.kwargs) + + def scan(self, package): + pass + -def _reg_http_method(func, method, **kwargs): - if not hasattr(func, '__aiohttp_web__'): - func.__aiohttp_web__ = {} - func.__aiohttp_web__[method] = kwargs - return func +def _make_route(method, path, handler, **kwargs): + return RouteInfo(method, path, handler, kwargs) -def head(**kwargs): - def wrapper(func): - @functools.wraps(func) +def _make_wrapper(method, path, kwargs): + def wrapper(handler): + @functools.wraps(handler) def wrapped(**kwargs): - return _reg_http_method(func, hdrs.METH_HEAD, **kwargs) + if hasattr(handler, '__aiohttp_web__'): + raise ValueError('Handler {handler!r} is registered already ' + 'as [{method}] {path} {kwargs}'.format( + handler=handler, + method=method, + path=path, + kwargs=kwargs)) + handler.__aiohttp_web__ = _make_route(method, path, + handler, kwargs) + return handler return wrapped return wrapper -def get(self, *args, name=None, allow_head=True, **kwargs): - """ - Shortcut for add_route with method GET, if allow_head is true another - route is added allowing head requests to the same endpoint - """ - if allow_head: - # it name is not None append -head to avoid it conflicting with - # the GET route below - head_name = name and '{}-head'.format(name) - self.add_route(hdrs.METH_HEAD, *args, name=head_name, **kwargs) - return self.add_route(hdrs.METH_GET, *args, name=name, **kwargs) +def head(path, handler=None, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_HEAD, path, **kwargs) + else: + return _make_route(hdrs.METH_HEAD, path, handler, **kwargs) -def add_post(self, *args, **kwargs): - """ - Shortcut for add_route with method POST - """ - return self.add_route(hdrs.METH_POST, *args, **kwargs) +def get(path, handler=None, *, name=None, allow_head=True, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_GET, path, name=name, + allow_head=allow_head, **kwargs) + else: + return _make_route(path, handler, name=name, + allow_head=allow_head, **kwargs) -def add_put(self, *args, **kwargs): - """ - Shortcut for add_route with method PUT - """ - return self.add_route(hdrs.METH_PUT, *args, **kwargs) +def post(path, handler=None, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_POST, path, **kwargs) + else: + return _make_route(hdrs.METH_POST, path, handler, **kwargs) -def add_patch(self, *args, **kwargs): - """ - Shortcut for add_route with method PATCH - """ - return self.add_route(hdrs.METH_PATCH, *args, **kwargs) +def put(path, handler=None, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_PUT, path, **kwargs) + else: + return _make_route(hdrs.METH_PUT, path, handler, **kwargs) -def add_delete(self, *args, **kwargs): - """ - Shortcut for add_route with method DELETE - """ - return self.add_route(hdrs.METH_DELETE, *args, **kwargs) +def patch(path, handler=None, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_PATCH, path, **kwargs) + else: + return _make_route(hdrs.METH_PATCH, path, handler, **kwargs) + + +def delete(path, handler=None, **kwargs): + if handler is None: + return _make_wrapper(hdrs.METH_DELETE, path, **kwargs) + else: + return _make_route(hdrs.METH_DELETE, path, handler, **kwargs) From 0c23ebf4933737096f5f4e66889ed80ca49a0dc0 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 20 Jun 2017 17:20:01 +0300 Subject: [PATCH 04/27] Make examples work --- aiohttp/web_urldispatcher.py | 77 +++++++++++++++------------------ examples/web_srv_route_deco.py | 60 +++++++++++++++++++++++++ examples/web_srv_route_table.py | 62 ++++++++++++++++++++++++++ 3 files changed, 158 insertions(+), 41 deletions(-) create mode 100644 examples/web_srv_route_deco.py create mode 100644 examples/web_srv_route_table.py diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 3848b8e9ef..259c43a4af 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -6,6 +6,7 @@ import keyword import os import re +import sys import warnings from collections import namedtuple from collections.abc import Container, Iterable, Sized @@ -30,7 +31,8 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View') + 'StaticResource', 'View', + 'head', 'get', 'post', 'patch', 'put', 'delete') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") PATH_SEP = re.escape('/') @@ -906,70 +908,63 @@ def add_routes(self, routes): reg(route.path, route.handler, **route.kwargs) def scan(self, package): - pass + prefix = package + '.' + for modname, mod in sys.modules.items(): + if modname == package or modname.startswith(prefix): + for name in dir(mod): + obj = getattr(mod, name) + route = getattr(obj, '__aiohttp_web__', None) + if route is not None: + reg = getattr(self, 'add_'+route.method.lower()) + reg(route.path, route.handler, **route.kwargs) def _make_route(method, path, handler, **kwargs): return RouteInfo(method, path, handler, kwargs) -def _make_wrapper(method, path, kwargs): +def _make_wrapper(method, path, **kwargs): def wrapper(handler): - @functools.wraps(handler) - def wrapped(**kwargs): - if hasattr(handler, '__aiohttp_web__'): - raise ValueError('Handler {handler!r} is registered already ' - 'as [{method}] {path} {kwargs}'.format( - handler=handler, - method=method, - path=path, - kwargs=kwargs)) - handler.__aiohttp_web__ = _make_route(method, path, - handler, kwargs) - return handler - return wrapped + if hasattr(handler, '__aiohttp_web__'): + raise ValueError('Handler {handler!r} is registered already ' + 'as [{method}] {path} {kwargs}'.format( + handler=handler, + method=method, + path=path, + kwargs=kwargs)) + handler.__aiohttp_web__ = _make_route(method, path, + handler, **kwargs) + return handler return wrapper -def head(path, handler=None, **kwargs): +def route(method, path, handler=None, **kwargs): if handler is None: - return _make_wrapper(hdrs.METH_HEAD, path, **kwargs) + return _make_wrapper(method, path, **kwargs) else: - return _make_route(hdrs.METH_HEAD, path, handler, **kwargs) + return _make_route(method, path, handler, **kwargs) + + +def head(path, handler=None, **kwargs): + return route(hdrs.METH_HEAD, path, handler, **kwargs) def get(path, handler=None, *, name=None, allow_head=True, **kwargs): - if handler is None: - return _make_wrapper(hdrs.METH_GET, path, name=name, - allow_head=allow_head, **kwargs) - else: - return _make_route(path, handler, name=name, - allow_head=allow_head, **kwargs) + return route(hdrs.METH_GET, path, handler, + allow_head=allow_head, **kwargs) def post(path, handler=None, **kwargs): - if handler is None: - return _make_wrapper(hdrs.METH_POST, path, **kwargs) - else: - return _make_route(hdrs.METH_POST, path, handler, **kwargs) + return route(hdrs.METH_POST, path, handler, **kwargs) def put(path, handler=None, **kwargs): - if handler is None: - return _make_wrapper(hdrs.METH_PUT, path, **kwargs) - else: - return _make_route(hdrs.METH_PUT, path, handler, **kwargs) + return route(hdrs.METH_PUT, path, handler, **kwargs) def patch(path, handler=None, **kwargs): - if handler is None: - return _make_wrapper(hdrs.METH_PATCH, path, **kwargs) - else: - return _make_route(hdrs.METH_PATCH, path, handler, **kwargs) + return route(hdrs.METH_PATCH, path, handler, **kwargs) def delete(path, handler=None, **kwargs): - if handler is None: - return _make_wrapper(hdrs.METH_DELETE, path, **kwargs) - else: - return _make_route(hdrs.METH_DELETE, path, handler, **kwargs) + return route(hdrs.METH_DELETE, path, handler, **kwargs) diff --git a/examples/web_srv_route_deco.py b/examples/web_srv_route_deco.py new file mode 100644 index 0000000000..b7ccb98914 --- /dev/null +++ b/examples/web_srv_route_deco.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Example for aiohttp.web basic server +with decorator definition for routes +""" + +import asyncio +import textwrap + +from aiohttp import web + + +@web.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 + + +@web.get('/simple') +async def simple(request): + return web.Response(text="Simple answer") + + +@web.get('/change_body') +async def change_body(request): + resp = web.Response() + resp.body = b"Body changed" + resp.content_type = 'text/plain' + return resp + + +@web.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.scan('__main__') + 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) From 6cab8b802106b9058d7c9c532a01ca0834910b96 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 20 Jun 2017 17:39:22 +0300 Subject: [PATCH 05/27] Refactor --- aiohttp/web_urldispatcher.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 259c43a4af..a9b44c5946 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -1,7 +1,6 @@ import abc import asyncio import collections -import functools import inspect import keyword import os @@ -37,7 +36,15 @@ HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") PATH_SEP = re.escape('/') -RouteInfo = namedtuple('RouteInfo', 'method, path, handler, kwargs') + +class RouteInfo(namedtuple('_RouteInfo', 'method, path, handler, kwargs')): + 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): @@ -903,9 +910,7 @@ def freeze(self): def add_routes(self, routes): for route in routes: - assert route.method in hdrs.METH_ALL - reg = getattr(self, 'add_'+route.method.lower()) - reg(route.path, route.handler, **route.kwargs) + route.register(self) def scan(self, package): prefix = package + '.' @@ -915,8 +920,7 @@ def scan(self, package): obj = getattr(mod, name) route = getattr(obj, '__aiohttp_web__', None) if route is not None: - reg = getattr(self, 'add_'+route.method.lower()) - reg(route.path, route.handler, **route.kwargs) + route.register(self) def _make_route(method, path, handler, **kwargs): From 8b44cdee38df7520dc672f7919f9e1d85fff0b56 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 20 Jun 2017 17:55:53 +0300 Subject: [PATCH 06/27] sort modules for scanning --- aiohttp/web_urldispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index a9b44c5946..900f9c1b32 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -914,7 +914,7 @@ def add_routes(self, routes): def scan(self, package): prefix = package + '.' - for modname, mod in sys.modules.items(): + for modname, mod in sorted(sys.modules.items()): if modname == package or modname.startswith(prefix): for name in dir(mod): obj = getattr(mod, name) From 1d5492c4d76108991c7c3fb442be2d3650e2644d Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 20 Jun 2017 18:37:09 +0300 Subject: [PATCH 07/27] Go forward --- aiohttp/web_urldispatcher.py | 3 +- tests/test_route_table.py | 105 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 tests/test_route_table.py diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 900f9c1b32..bd4fd75ae3 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -31,7 +31,7 @@ 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', 'StaticResource', 'View', - 'head', 'get', 'post', 'patch', 'put', 'delete') + 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") PATH_SEP = re.escape('/') @@ -909,6 +909,7 @@ def freeze(self): resource.freeze() def add_routes(self, routes): + # TODO: add_table maybe? for route in routes: route.register(self) diff --git a/tests/test_route_table.py b/tests/test_route_table.py new file mode 100644 index 0000000000..33ba97f72a --- /dev/null +++ b/tests/test_route_table.py @@ -0,0 +1,105 @@ +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())[0] + assert route.handler is handler + assert route.method == 'GET' + + route2 = list(router.routes())[1] + 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' + + +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' + + +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' + + +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' + + +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' + + +def test_route(router): + @asyncio.coroutine + def handler(request): + pass + + router.add_routes([web.route('OPTIONS', '/', handler)]) + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.handler is handler + assert route.method == 'OPTIONS' From 3eb5137cc4f84b0b8590e035add7a8816a09a2cd Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Tue, 20 Jun 2017 19:56:24 +0300 Subject: [PATCH 08/27] Add tests --- tests/test_route_table.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_route_table.py b/tests/test_route_table.py index 33ba97f72a..3fc1813c87 100644 --- a/tests/test_route_table.py +++ b/tests/test_route_table.py @@ -19,11 +19,11 @@ def handler(request): router.add_routes([web.get('/', handler)]) assert len(router.routes()) == 2 # GET and HEAD - route = list(router.routes())[0] + route = list(router.routes())[1] assert route.handler is handler assert route.method == 'GET' - route2 = list(router.routes())[1] + route2 = list(router.routes())[0] assert route2.handler is handler assert route2.method == 'HEAD' @@ -97,9 +97,9 @@ def test_route(router): def handler(request): pass - router.add_routes([web.route('OPTIONS', '/', handler)]) + 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 == 'OPTIONS' + assert route.method == 'OTHER' From a5c24a044669b51db804a0365997004a3f312943 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 21 Jun 2017 11:00:55 +0300 Subject: [PATCH 09/27] Add test for decoration methods --- tests/test_route_table.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_route_table.py b/tests/test_route_table.py index 3fc1813c87..b1722add64 100644 --- a/tests/test_route_table.py +++ b/tests/test_route_table.py @@ -22,6 +22,7 @@ def handler(request): 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 @@ -39,6 +40,7 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'HEAD' + assert str(route.url_for()) == '/' def test_post(router): @@ -51,6 +53,7 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'POST' + assert str(route.url_for()) == '/' def test_put(router): @@ -64,6 +67,7 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'PUT' + assert str(route.url_for()) == '/' def test_patch(router): @@ -77,6 +81,7 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'PATCH' + assert str(route.url_for()) == '/' def test_delete(router): @@ -90,6 +95,7 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'DELETE' + assert str(route.url_for()) == '/' def test_route(router): @@ -103,3 +109,4 @@ def handler(request): route = list(router.routes())[0] assert route.handler is handler assert route.method == 'OTHER' + assert str(route.url_for()) == '/' From 910cf1c56760604d9fc738f388c27aca9ea7bfdc Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 21 Jun 2017 14:47:29 +0300 Subject: [PATCH 10/27] Add missing file --- tests/test_route_deco.py | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_route_deco.py diff --git a/tests/test_route_deco.py b/tests/test_route_deco.py new file mode 100644 index 0000000000..48f4f2b27d --- /dev/null +++ b/tests/test_route_deco.py @@ -0,0 +1,64 @@ +import asyncio +import sys +from importlib.machinery import ModuleSpec, SourceFileLoader +from importlib.util import module_from_spec +from textwrap import dedent + +import pytest + +from aiohttp import web +from aiohttp.web_urldispatcher import UrlDispatcher + + +@pytest.fixture +def router(): + return UrlDispatcher() + + +def test_add_routeinfo(router): + @web.get('/path') + @asyncio.coroutine + def handler(request): + pass + + assert hasattr(handler, '__aiohttp_web__') + info = handler.__aiohttp_web__ + assert info.method == 'GET' + assert info.path == '/path' + assert info.handler is handler + + +def test_add_routeinfo_twice(router): + with pytest.raises(ValueError): + @web.get('/path') + @web.post('/path') + @asyncio.coroutine + def handler(request): + pass + + +def test_scan(router): + loader = SourceFileLoader('', '') + spec = ModuleSpec('aiohttp.tmp_test', loader, is_package=False) + mod = module_from_spec(spec) + sys.modules[mod.__name__] = mod + content = dedent("""\ + import asyncio + from aiohttp import web + + @web.head('/path') + @asyncio.coroutine + def handler(request): + pass + """) + try: + exec(content, mod.__dict__) + router.scan(mod.__name__) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'HEAD' + assert str(route.url_for()) == '/path' + finally: + del sys.modules[mod.__name__] From 36d4fc0faec54148baca67fd9dc1d5789fe49198 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 22 Jun 2017 22:18:16 +0300 Subject: [PATCH 11/27] Fix python 3.4, add test --- tests/test_route_deco.py | 94 +++++++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 16 deletions(-) diff --git a/tests/test_route_deco.py b/tests/test_route_deco.py index 48f4f2b27d..7c67d5beb3 100644 --- a/tests/test_route_deco.py +++ b/tests/test_route_deco.py @@ -1,7 +1,5 @@ import asyncio import sys -from importlib.machinery import ModuleSpec, SourceFileLoader -from importlib.util import module_from_spec from textwrap import dedent import pytest @@ -10,6 +8,39 @@ from aiohttp.web_urldispatcher import UrlDispatcher +if sys.version_info >= (3, 5): + @pytest.fixture + def create_module(): + from importlib.machinery import ModuleSpec, SourceFileLoader + from importlib.util import module_from_spec + mods = [] + + def maker(name, *, is_package=False): + loader = SourceFileLoader('', '') + spec = ModuleSpec(name, loader, is_package=is_package) + mod = module_from_spec(spec) + sys.modules[mod.__name__] = mod + mods.append(mod) + return mod + yield maker + for mod in mods: + del sys.modules[mod.__name__] +else: + @pytest.fixture + def create_module(name): + from imp import new_module + + mods = [] + + def maker(name, *, is_package=False): + mod = new_module(name) + sys.modules[mod.__name__] = mod + mods.append(mod) + yield maker + for mod in mods: + del sys.modules[mod.__name__] + + @pytest.fixture def router(): return UrlDispatcher() @@ -37,11 +68,8 @@ def handler(request): pass -def test_scan(router): - loader = SourceFileLoader('', '') - spec = ModuleSpec('aiohttp.tmp_test', loader, is_package=False) - mod = module_from_spec(spec) - sys.modules[mod.__name__] = mod +def test_scan_mod(router, create_module): + mod = create_module('aiohttp.tmp.test_mod') content = dedent("""\ import asyncio from aiohttp import web @@ -51,14 +79,48 @@ def test_scan(router): def handler(request): pass """) - try: - exec(content, mod.__dict__) - router.scan(mod.__name__) + exec(content, mod.__dict__) + router.scan(mod.__name__) + + assert len(router.routes()) == 1 + + route = list(router.routes())[0] + assert route.method == 'HEAD' + assert str(route.url_for()) == '/path' + + +def test_scan_package(router, create_module): + mod = create_module('aiohttp.tmp', is_package=True) + mod1 = create_module('aiohttp.tmp.test_mod1') + content1 = dedent("""\ + import asyncio + from aiohttp import web + + @web.head('/path1') + @asyncio.coroutine + def handler(request): + pass + """) + exec(content1, mod1.__dict__) + mod2 = create_module('aiohttp.tmp.test_mod2') + content2 = dedent("""\ + import asyncio + from aiohttp import web + + @web.put('/path2') + @asyncio.coroutine + def handler(request): + pass + """) + exec(content2, mod2.__dict__) + router.scan(mod.__package__) + + assert len(router.routes()) == 2 - assert len(router.routes()) == 1 + route1 = list(router.routes())[0] + assert route1.method == 'HEAD' + assert str(route1.url_for()) == '/path1' - route = list(router.routes())[0] - assert route.method == 'HEAD' - assert str(route.url_for()) == '/path' - finally: - del sys.modules[mod.__name__] + route1 = list(router.routes())[1] + assert route1.method == 'PUT' + assert str(route1.url_for()) == '/path2' From de48fdaae66ad388c236cb1ff3828457f428143e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 22 Jun 2017 23:08:13 +0300 Subject: [PATCH 12/27] Fix typo --- tests/test_route_deco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_route_deco.py b/tests/test_route_deco.py index 7c67d5beb3..4584fb6323 100644 --- a/tests/test_route_deco.py +++ b/tests/test_route_deco.py @@ -27,7 +27,7 @@ def maker(name, *, is_package=False): del sys.modules[mod.__name__] else: @pytest.fixture - def create_module(name): + def create_module(): from imp import new_module mods = [] From 234ed0e0c1278aaebcc4c5d1439a65482a0b0462 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 00:00:35 +0300 Subject: [PATCH 13/27] Implement RouteDef --- aiohttp/web_urldispatcher.py | 97 ++++++++------ tests/test_route_deco.py | 126 ------------------ ...{test_route_table.py => test_route_def.py} | 123 +++++++++++++++++ 3 files changed, 178 insertions(+), 168 deletions(-) delete mode 100644 tests/test_route_deco.py rename tests/{test_route_table.py => test_route_def.py} (50%) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index bd4fd75ae3..59674b6113 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -5,10 +5,9 @@ import keyword import os import re -import sys import warnings from collections import namedtuple -from collections.abc import Container, Iterable, Sized +from collections.abc import Container, Iterable, Sequence, Sized from functools import wraps from pathlib import Path from types import MappingProxyType @@ -30,7 +29,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', + 'StaticResource', 'View', 'RouteDef', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") @@ -909,67 +908,81 @@ def freeze(self): resource.freeze() def add_routes(self, routes): + """Append routes to route table. + + Parameter should be a sequence of RouteInfo objects. + """ # TODO: add_table maybe? for route in routes: route.register(self) - def scan(self, package): - prefix = package + '.' - for modname, mod in sorted(sys.modules.items()): - if modname == package or modname.startswith(prefix): - for name in dir(mod): - obj = getattr(mod, name) - route = getattr(obj, '__aiohttp_web__', None) - if route is not None: - route.register(self) - -def _make_route(method, path, handler, **kwargs): +def route(method, path, handler, **kwargs): return RouteInfo(method, path, handler, kwargs) -def _make_wrapper(method, path, **kwargs): - def wrapper(handler): - if hasattr(handler, '__aiohttp_web__'): - raise ValueError('Handler {handler!r} is registered already ' - 'as [{method}] {path} {kwargs}'.format( - handler=handler, - method=method, - path=path, - kwargs=kwargs)) - handler.__aiohttp_web__ = _make_route(method, path, - handler, **kwargs) - return handler - return wrapper - - -def route(method, path, handler=None, **kwargs): - if handler is None: - return _make_wrapper(method, path, **kwargs) - else: - return _make_route(method, path, handler, **kwargs) - - -def head(path, handler=None, **kwargs): +def head(path, handler, **kwargs): return route(hdrs.METH_HEAD, path, handler, **kwargs) -def get(path, handler=None, *, name=None, allow_head=True, **kwargs): +def get(path, handler, *, name=None, allow_head=True, **kwargs): return route(hdrs.METH_GET, path, handler, allow_head=allow_head, **kwargs) -def post(path, handler=None, **kwargs): +def post(path, handler, **kwargs): return route(hdrs.METH_POST, path, handler, **kwargs) -def put(path, handler=None, **kwargs): +def put(path, handler, **kwargs): return route(hdrs.METH_PUT, path, handler, **kwargs) -def patch(path, handler=None, **kwargs): +def patch(path, handler, **kwargs): return route(hdrs.METH_PATCH, path, handler, **kwargs) -def delete(path, handler=None, **kwargs): +def delete(path, handler, **kwargs): return route(hdrs.METH_DELETE, path, handler, **kwargs) + + +class RouteDef(Sequence): + """Route definition table""" + def __init__(self): + 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(RouteInfo(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/tests/test_route_deco.py b/tests/test_route_deco.py deleted file mode 100644 index 4584fb6323..0000000000 --- a/tests/test_route_deco.py +++ /dev/null @@ -1,126 +0,0 @@ -import asyncio -import sys -from textwrap import dedent - -import pytest - -from aiohttp import web -from aiohttp.web_urldispatcher import UrlDispatcher - - -if sys.version_info >= (3, 5): - @pytest.fixture - def create_module(): - from importlib.machinery import ModuleSpec, SourceFileLoader - from importlib.util import module_from_spec - mods = [] - - def maker(name, *, is_package=False): - loader = SourceFileLoader('', '') - spec = ModuleSpec(name, loader, is_package=is_package) - mod = module_from_spec(spec) - sys.modules[mod.__name__] = mod - mods.append(mod) - return mod - yield maker - for mod in mods: - del sys.modules[mod.__name__] -else: - @pytest.fixture - def create_module(): - from imp import new_module - - mods = [] - - def maker(name, *, is_package=False): - mod = new_module(name) - sys.modules[mod.__name__] = mod - mods.append(mod) - yield maker - for mod in mods: - del sys.modules[mod.__name__] - - -@pytest.fixture -def router(): - return UrlDispatcher() - - -def test_add_routeinfo(router): - @web.get('/path') - @asyncio.coroutine - def handler(request): - pass - - assert hasattr(handler, '__aiohttp_web__') - info = handler.__aiohttp_web__ - assert info.method == 'GET' - assert info.path == '/path' - assert info.handler is handler - - -def test_add_routeinfo_twice(router): - with pytest.raises(ValueError): - @web.get('/path') - @web.post('/path') - @asyncio.coroutine - def handler(request): - pass - - -def test_scan_mod(router, create_module): - mod = create_module('aiohttp.tmp.test_mod') - content = dedent("""\ - import asyncio - from aiohttp import web - - @web.head('/path') - @asyncio.coroutine - def handler(request): - pass - """) - exec(content, mod.__dict__) - router.scan(mod.__name__) - - assert len(router.routes()) == 1 - - route = list(router.routes())[0] - assert route.method == 'HEAD' - assert str(route.url_for()) == '/path' - - -def test_scan_package(router, create_module): - mod = create_module('aiohttp.tmp', is_package=True) - mod1 = create_module('aiohttp.tmp.test_mod1') - content1 = dedent("""\ - import asyncio - from aiohttp import web - - @web.head('/path1') - @asyncio.coroutine - def handler(request): - pass - """) - exec(content1, mod1.__dict__) - mod2 = create_module('aiohttp.tmp.test_mod2') - content2 = dedent("""\ - import asyncio - from aiohttp import web - - @web.put('/path2') - @asyncio.coroutine - def handler(request): - pass - """) - exec(content2, mod2.__dict__) - router.scan(mod.__package__) - - assert len(router.routes()) == 2 - - route1 = list(router.routes())[0] - assert route1.method == 'HEAD' - assert str(route1.url_for()) == '/path1' - - route1 = list(router.routes())[1] - assert route1.method == 'PUT' - assert str(route1.url_for()) == '/path2' diff --git a/tests/test_route_table.py b/tests/test_route_def.py similarity index 50% rename from tests/test_route_table.py rename to tests/test_route_def.py index b1722add64..3daa4ce6e2 100644 --- a/tests/test_route_table.py +++ b/tests/test_route_def.py @@ -110,3 +110,126 @@ def handler(request): assert route.handler is handler assert route.method == 'OTHER' assert str(route.url_for()) == '/' + + +def test_head_deco(router): + routes = web.RouteDef() + + @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.RouteDef() + + @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.RouteDef() + + @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.RouteDef() + + @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.RouteDef() + + @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.RouteDef() + + @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.RouteDef() + + @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' From d158513e9e39f4e2c439241ef70b1d7f156d7e17 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 00:57:22 +0300 Subject: [PATCH 14/27] Test cover --- aiohttp/web_urldispatcher.py | 2 +- examples/web_srv_route_deco.py | 13 ++++++++----- tests/test_route_def.py | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 59674b6113..085824b397 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -29,7 +29,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', 'RouteDef', + 'StaticResource', 'View', 'RouteDef', 'RouteInfo', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") diff --git a/examples/web_srv_route_deco.py b/examples/web_srv_route_deco.py index b7ccb98914..a485d6888d 100644 --- a/examples/web_srv_route_deco.py +++ b/examples/web_srv_route_deco.py @@ -9,7 +9,10 @@ from aiohttp import web -@web.get('/') +routes = web.RouteDef() + + +@routes.get('/') async def intro(request): txt = textwrap.dedent("""\ Type {url}/hello/John {url}/simple or {url}/change_body @@ -24,12 +27,12 @@ async def intro(request): return resp -@web.get('/simple') +@routes.get('/simple') async def simple(request): return web.Response(text="Simple answer") -@web.get('/change_body') +@routes.get('/change_body') async def change_body(request): resp = web.Response() resp.body = b"Body changed" @@ -37,7 +40,7 @@ async def change_body(request): return resp -@web.get('/hello') +@routes.get('/hello') async def hello(request): resp = web.StreamResponse() name = request.match_info.get('name', 'Anonymous') @@ -52,7 +55,7 @@ async def hello(request): async def init(): app = web.Application() - app.router.scan('__main__') + app.router.add_routes(routes) return app loop = asyncio.get_event_loop() diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 3daa4ce6e2..5586986505 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -233,3 +233,19 @@ def handler(request): route = list(router.routes())[0] assert route.method == 'OTHER' assert str(route.url_for()) == '/path' + + +def test_routedef_sequence_protocol(): + routes = web.RouteDef() + + @routes.delete('/path') + @asyncio.coroutine + def handler(request): + pass + + assert len(routes) == 1 + + info = routes[0] + assert isinstance(info, web.RouteInfo) + assert info in routes + assert list(routes)[0] is info From b8ad2f48120c168d16a410bd1f59e766769c84e9 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 01:17:03 +0300 Subject: [PATCH 15/27] RouteDef -> RoutesDef --- aiohttp/web_urldispatcher.py | 4 ++-- examples/web_srv_route_deco.py | 2 +- tests/test_route_def.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 085824b397..10a223fe7d 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -29,7 +29,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', 'RouteDef', 'RouteInfo', + 'StaticResource', 'View', 'RoutesDef', 'RouteInfo', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") @@ -946,7 +946,7 @@ def delete(path, handler, **kwargs): return route(hdrs.METH_DELETE, path, handler, **kwargs) -class RouteDef(Sequence): +class RoutesDef(Sequence): """Route definition table""" def __init__(self): self._items = [] diff --git a/examples/web_srv_route_deco.py b/examples/web_srv_route_deco.py index a485d6888d..250d24ed71 100644 --- a/examples/web_srv_route_deco.py +++ b/examples/web_srv_route_deco.py @@ -9,7 +9,7 @@ from aiohttp import web -routes = web.RouteDef() +routes = web.RoutesDef() @routes.get('/') diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 5586986505..1c422b7a7b 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -113,7 +113,7 @@ def handler(request): def test_head_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.head('/path') @asyncio.coroutine @@ -130,7 +130,7 @@ def handler(request): def test_get_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.get('/path') @asyncio.coroutine @@ -151,7 +151,7 @@ def handler(request): def test_post_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.post('/path') @asyncio.coroutine @@ -168,7 +168,7 @@ def handler(request): def test_put_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.put('/path') @asyncio.coroutine @@ -185,7 +185,7 @@ def handler(request): def test_patch_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.patch('/path') @asyncio.coroutine @@ -202,7 +202,7 @@ def handler(request): def test_delete_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.delete('/path') @asyncio.coroutine @@ -219,7 +219,7 @@ def handler(request): def test_route_deco(router): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.route('OTHER', '/path') @asyncio.coroutine @@ -236,7 +236,7 @@ def handler(request): def test_routedef_sequence_protocol(): - routes = web.RouteDef() + routes = web.RoutesDef() @routes.delete('/path') @asyncio.coroutine From 7391a69dc1ff44131e98013a916078c07e956e50 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 02:27:02 +0300 Subject: [PATCH 16/27] RouteInfo -> RouteDef --- aiohttp/web_urldispatcher.py | 10 +++++----- tests/test_route_def.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 10a223fe7d..1599ad9607 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -29,14 +29,14 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', 'RoutesDef', 'RouteInfo', + 'StaticResource', 'View', 'RoutesDef', 'RouteDef', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") PATH_SEP = re.escape('/') -class RouteInfo(namedtuple('_RouteInfo', 'method, path, handler, kwargs')): +class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): def register(self, router): if self.method in hdrs.METH_ALL: reg = getattr(router, 'add_'+self.method.lower()) @@ -910,7 +910,7 @@ def freeze(self): def add_routes(self, routes): """Append routes to route table. - Parameter should be a sequence of RouteInfo objects. + Parameter should be a sequence of RouteDef objects. """ # TODO: add_table maybe? for route in routes: @@ -918,7 +918,7 @@ def add_routes(self, routes): def route(method, path, handler, **kwargs): - return RouteInfo(method, path, handler, kwargs) + return RouteDef(method, path, handler, kwargs) def head(path, handler, **kwargs): @@ -965,7 +965,7 @@ def __contains__(self, item): def route(self, method, path, **kwargs): def inner(handler): - self._items.append(RouteInfo(method, path, handler, kwargs)) + self._items.append(RouteDef(method, path, handler, kwargs)) return handler return inner diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 1c422b7a7b..1251927bb3 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -246,6 +246,6 @@ def handler(request): assert len(routes) == 1 info = routes[0] - assert isinstance(info, web.RouteInfo) + assert isinstance(info, web.RouteDef) assert info in routes assert list(routes)[0] is info From 341da53142f765b3d3599b7df310ff3f5527a959 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 02:29:20 +0300 Subject: [PATCH 17/27] Add couple TODOs, drop RouteDef from exported names --- aiohttp/web_urldispatcher.py | 6 +++++- tests/test_route_def.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 1599ad9607..1dbfe7ead5 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -29,7 +29,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', 'RoutesDef', 'RouteDef', + 'StaticResource', 'View', 'RoutesDef', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") @@ -37,6 +37,8 @@ class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): + # TODO: add __repr__ + def register(self, router): if self.method in hdrs.METH_ALL: reg = getattr(router, 'add_'+self.method.lower()) @@ -951,6 +953,8 @@ class RoutesDef(Sequence): def __init__(self): self._items = [] + # TODO: add __repr__ + def __getitem__(self, index): return self._items[index] diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 1251927bb3..85b0271f13 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -3,7 +3,7 @@ import pytest from aiohttp import web -from aiohttp.web_urldispatcher import UrlDispatcher +from aiohttp.web_urldispatcher import UrlDispatcher, RouteDef @pytest.fixture @@ -246,6 +246,6 @@ def handler(request): assert len(routes) == 1 info = routes[0] - assert isinstance(info, web.RouteDef) + assert isinstance(info, RouteDef) assert info in routes assert list(routes)[0] is info From ec0630af98a8da2be7f5f5f1897397225a9d315e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 23 Jun 2017 07:37:34 +0300 Subject: [PATCH 18/27] Fix flake8 blame --- tests/test_route_def.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 85b0271f13..078be1ab00 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -3,7 +3,7 @@ import pytest from aiohttp import web -from aiohttp.web_urldispatcher import UrlDispatcher, RouteDef +from aiohttp.web_urldispatcher import RouteDef, UrlDispatcher @pytest.fixture From 9c1845ccafe4bb698fc27cd9879d4dbfb5c2260e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 10:40:50 +0300 Subject: [PATCH 19/27] RoutesDef -> RouteTableDef --- aiohttp/web_urldispatcher.py | 4 ++-- examples/web_srv_route_deco.py | 2 +- tests/test_route_def.py | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 1dbfe7ead5..daf834a47c 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -29,7 +29,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View', 'RoutesDef', + 'StaticResource', 'View', 'RouteDef', 'RouteTableDef', 'head', 'get', 'post', 'patch', 'put', 'delete', 'route') HTTP_METHOD_RE = re.compile(r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$") @@ -948,7 +948,7 @@ def delete(path, handler, **kwargs): return route(hdrs.METH_DELETE, path, handler, **kwargs) -class RoutesDef(Sequence): +class RouteTableDef(Sequence): """Route definition table""" def __init__(self): self._items = [] diff --git a/examples/web_srv_route_deco.py b/examples/web_srv_route_deco.py index 250d24ed71..9accbf6e38 100644 --- a/examples/web_srv_route_deco.py +++ b/examples/web_srv_route_deco.py @@ -9,7 +9,7 @@ from aiohttp import web -routes = web.RoutesDef() +routes = web.RouteTableDef() @routes.get('/') diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 078be1ab00..61f7c55643 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -3,7 +3,7 @@ import pytest from aiohttp import web -from aiohttp.web_urldispatcher import RouteDef, UrlDispatcher +from aiohttp.web_urldispatcher import UrlDispatcher @pytest.fixture @@ -113,7 +113,7 @@ def handler(request): def test_head_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.head('/path') @asyncio.coroutine @@ -130,7 +130,7 @@ def handler(request): def test_get_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.get('/path') @asyncio.coroutine @@ -151,7 +151,7 @@ def handler(request): def test_post_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.post('/path') @asyncio.coroutine @@ -168,7 +168,7 @@ def handler(request): def test_put_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.put('/path') @asyncio.coroutine @@ -185,7 +185,7 @@ def handler(request): def test_patch_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.patch('/path') @asyncio.coroutine @@ -202,7 +202,7 @@ def handler(request): def test_delete_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.delete('/path') @asyncio.coroutine @@ -219,7 +219,7 @@ def handler(request): def test_route_deco(router): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.route('OTHER', '/path') @asyncio.coroutine @@ -236,7 +236,7 @@ def handler(request): def test_routedef_sequence_protocol(): - routes = web.RoutesDef() + routes = web.RouteTableDef() @routes.delete('/path') @asyncio.coroutine @@ -246,6 +246,6 @@ def handler(request): assert len(routes) == 1 info = routes[0] - assert isinstance(info, RouteDef) + assert isinstance(info, web.RouteDef) assert info in routes assert list(routes)[0] is info From e2bc86907f1d7c965625ab770c4caa12b4bd504f Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 11:18:56 +0300 Subject: [PATCH 20/27] Add reprs --- aiohttp/web_urldispatcher.py | 11 +++++++++-- tests/test_route_def.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 442248f846..3d06e012a6 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -38,7 +38,13 @@ class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): - # TODO: add __repr__ + def __repr__(self): + info = [] + for name, value in sorted(self.kwargs.items()): + info += ", {}={}".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: @@ -956,7 +962,8 @@ class RouteTableDef(Sequence): def __init__(self): self._items = [] - # TODO: add __repr__ + def __repr__(self): + return "".format(len(self._items)) def __getitem__(self, index): return self._items[index] diff --git a/tests/test_route_def.py b/tests/test_route_def.py index 61f7c55643..aab34fc394 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -249,3 +249,26 @@ def handler(request): 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_table_def(): + routes = web.RouteTableDef() + + @routes.get('/path') + @asyncio.coroutine + def handler(request): + pass + + assert repr(routes) == "" From 58686cbc6742fca5e84355d730de2bb81ec1164c Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 11:30:24 +0300 Subject: [PATCH 21/27] Add changes record --- changes/2004.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2004.feature 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. From 875ae8e09cee5c7e78f3031bbbc6c0d0dc501cb0 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 12:37:45 +0300 Subject: [PATCH 22/27] Test cover missed case --- aiohttp/web_urldispatcher.py | 2 +- tests/test_route_def.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 3d06e012a6..50101f9379 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -41,7 +41,7 @@ class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): def __repr__(self): info = [] for name, value in sorted(self.kwargs.items()): - info += ", {}={}".format(name, value) + info += ", {}={!r}".format(name, value) return (" {handler.__name__!r}" "{info}>".format(method=self.method, path=self.path, handler=self.handler, info=''.join(info))) diff --git a/tests/test_route_def.py b/tests/test_route_def.py index aab34fc394..730f73da6a 100644 --- a/tests/test_route_def.py +++ b/tests/test_route_def.py @@ -263,6 +263,18 @@ def handler(request): 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() From 7c54e3678266ff42565f31d58de5b145bb6d30e4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 15:30:25 +0300 Subject: [PATCH 23/27] Add documentation for new route definitions API in web reference --- aiohttp/web_urldispatcher.py | 2 +- docs/web_reference.rst | 186 ++++++++++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 2 deletions(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 50101f9379..b2df179dfa 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -937,7 +937,7 @@ def head(path, handler, **kwargs): def get(path, handler, *, name=None, allow_head=True, **kwargs): - return route(hdrs.METH_GET, path, handler, + return route(hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 1a25f0b519..d6864df9db 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,181 @@ and *405 Method Not Allowed*. HTTP status reason +RouteDef +^^^^^^^^ + +Route definition, a description for not registered yet route. + +Could be used for filing route table by providig 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 + +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 mutable 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 ^^^^^^^^^ From 48203d3c1216ea9339cb40f495a1ac6bc800333c Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 16:59:34 +0300 Subject: [PATCH 24/27] Fix typo --- docs/web_reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index d6864df9db..1d19c50bec 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2033,7 +2033,7 @@ RouteDef Route definition, a description for not registered yet route. -Could be used for filing route table by providig a list of 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 From 68f72256d4a6e06b29f8d08af9cf2b5368befd34 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 19:09:11 +0300 Subject: [PATCH 25/27] Mention route tables and route decorators in web usage --- docs/web.rst | 188 +++++++++++++++++++++++++++-------------- docs/web_reference.rst | 7 +- 2 files changed, 131 insertions(+), 64 deletions(-) diff --git a/docs/web.rst b/docs/web.rst index 3e121c80cb..b0ed9d7dc7 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,68 @@ 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, route table 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 +534,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 +1105,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 1d19c50bec..84db0e1b22 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2028,6 +2028,9 @@ and *405 Method Not Allowed*. HTTP status reason +.. _aiohttp-web-route-def: + + RouteDef ^^^^^^^^ @@ -2128,6 +2131,8 @@ The definition is created by functions like :func:`get` or .. versionadded:: 2.3 +.. _aiohttp-web-route-table-def: + RouteTableDef ^^^^^^^^^^^^^ @@ -2151,7 +2156,7 @@ A routes table definition used for describing routes by decorators .. class:: RouteTableDef() - A mutable sequence of :class:`RouteDef` instances (implements + A sequence of :class:`RouteDef` instances (implements :class:`abc.collections.Sequence` protocol). In addition to all standard :class:`list` methods the class From aaab3ed1badb6336d10ba60dc084a4f63e087c03 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 2 Aug 2017 19:24:54 +0300 Subject: [PATCH 26/27] Text flow polishing --- docs/web.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/web.rst b/docs/web.rst index b0ed9d7dc7..d0b7384a83 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -377,8 +377,9 @@ registered *route definitions* into application's router. .. seealso:: :ref:`aiohttp-web-route-table-def` reference. -All tree ways (imperative, route table and decorators) are equivalent, -you could use what do you prefer or even mix them on your own. +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 From c10f4e06dd2a733b6020d21eec9696f36c54cfc4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 3 Aug 2017 13:38:18 +0300 Subject: [PATCH 27/27] Fix typo --- aiohttp/web_urldispatcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index b2df179dfa..d4e6845118 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -41,7 +41,7 @@ class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): def __repr__(self): info = [] for name, value in sorted(self.kwargs.items()): - info += ", {}={!r}".format(name, value) + info.append(", {}={!r}".format(name, value)) return (" {handler.__name__!r}" "{info}>".format(method=self.method, path=self.path, handler=self.handler, info=''.join(info)))