diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 910f346e2c8..bd4fd75ae31 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -5,7 +5,9 @@ import keyword import os import re +import sys import warnings +from collections import namedtuple from collections.abc import Container, Iterable, Sized from functools import wraps from pathlib import Path @@ -28,12 +30,23 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', 'AbstractResource', 'Resource', 'PlainResource', 'DynamicResource', 'AbstractRoute', 'ResourceRoute', - 'StaticResource', 'View') + 'StaticResource', 'View', + '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')): + 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): @@ -894,3 +907,69 @@ def freeze(self): super().freeze() for resource in self._resources: resource.freeze() + + def add_routes(self, routes): + # 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): + 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): + return route(hdrs.METH_HEAD, path, handler, **kwargs) + + +def get(path, handler=None, *, name=None, allow_head=True, **kwargs): + return route(hdrs.METH_GET, path, handler, + allow_head=allow_head, **kwargs) + + +def post(path, handler=None, **kwargs): + return route(hdrs.METH_POST, path, handler, **kwargs) + + +def put(path, handler=None, **kwargs): + return route(hdrs.METH_PUT, path, handler, **kwargs) + + +def patch(path, handler=None, **kwargs): + return route(hdrs.METH_PATCH, path, handler, **kwargs) + + +def delete(path, handler=None, **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 00000000000..b7ccb989141 --- /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 00000000000..7d8af62a5c2 --- /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_deco.py b/tests/test_route_deco.py new file mode 100644 index 00000000000..4584fb6323b --- /dev/null +++ b/tests/test_route_deco.py @@ -0,0 +1,126 @@ +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_table.py new file mode 100644 index 00000000000..b1722add64c --- /dev/null +++ b/tests/test_route_table.py @@ -0,0 +1,112 @@ +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()) == '/'