From bc6898373851bac26c54f47fb07eb164a9ff1eb7 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 16 Dec 2015 02:34:08 +0200 Subject: [PATCH 1/8] Add example for class based view --- examples/web_classview1.py | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 examples/web_classview1.py diff --git a/examples/web_classview1.py b/examples/web_classview1.py new file mode 100644 index 00000000000..7cf609c3b03 --- /dev/null +++ b/examples/web_classview1.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Example for aiohttp.web class based views +""" + + +import asyncio +from aiohttp import hdrs +from aiohttp.web import (json_response, Application, Response, + HTTPMethodNotAllowed) + + +ALL_METHODS = {hdrs.METH_CONNECT, hdrs.METH_HEAD, hdrs.METH_GET, + hdrs.METH_DELETE, hdrs.METH_OPTIONS, hdrs.METH_PATCH, + hdrs.METH_POST, hdrs.METH_PUT, hdrs.METH_TRACE} + + +class BaseView: + def __init__(self, request): + self.request = request + + def __await__(self): + method = getattr(self, self.request.method, None) + if method is None: + allowed_methods = {m for m in ALL_METHODS if hasattr(self, m)} + return HTTPMethodNotAllowed(self.request.method, allowed_methods) + resp = method().__await__() + return resp + + +class View(BaseView): + + async def GET(self): + return Response(text='OK') + + +async def index(request): + txt = """ + + + Class based view example + + +

Class based view example

+ + + + """ + return Response(text=txt, content_type='text/html') + + +async def init(loop): + app = Application(loop=loop) + app.router.add_route('GET', '/', index) + app.router.add_route('GET', '/get', View) + app.router.add_route('POST', '/post', View) + app.router.add_route('PUT', '/put', View) + app.router.add_route('DELETE', '/delete', View) + + handler = app.make_handler() + srv = await loop.create_server(handler, '127.0.0.1', 8080) + print("Server started at http://127.0.0.1:8080") + return srv, handler + + +loop = asyncio.get_event_loop() +srv, handler = loop.run_until_complete(init(loop)) +try: + loop.run_forever() +except KeyboardInterrupt: + loop.run_until_complete(handler.finish_connections()) From 00d52084224a4965594e8a1ee13ca821a4424cf4 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 16 Dec 2015 02:41:28 +0200 Subject: [PATCH 2/8] Fix spaces --- examples/web_classview1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/web_classview1.py b/examples/web_classview1.py index 7cf609c3b03..a620952388b 100644 --- a/examples/web_classview1.py +++ b/examples/web_classview1.py @@ -34,7 +34,7 @@ async def GET(self): async def index(request): - txt = """ + txt = """ Class based view example From 9196a5415271125da1a92771e2007e46de93c3c2 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 16 Dec 2015 03:15:52 +0200 Subject: [PATCH 3/8] Finish CBV example --- examples/web_classview1.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/web_classview1.py b/examples/web_classview1.py index a620952388b..b4f9d1828b8 100644 --- a/examples/web_classview1.py +++ b/examples/web_classview1.py @@ -4,6 +4,8 @@ import asyncio +import functools +import json from aiohttp import hdrs from aiohttp.web import (json_response, Application, Response, HTTPMethodNotAllowed) @@ -30,7 +32,20 @@ def __await__(self): class View(BaseView): async def GET(self): - return Response(text='OK') + return json_response({ + 'method': 'get', + 'args': dict(self.request.GET), + 'headers': dict(self.request.headers), + }, dumps=functools.partial(json.dumps, indent=4)) + + async def POST(self): + data = await self.request.post() + return json_response({ + 'method': 'post', + 'args': dict(self.request.GET), + 'data': dict(data), + 'headers': dict(self.request.headers), + }, dumps=functools.partial(json.dumps, indent=4)) async def index(request): @@ -45,8 +60,6 @@ async def index(request):
  • / This page
  • /get Returns GET data.
  • /post Returns POST data. -
  • /put Returns PUT data. -
  • /delete Returns DELETE data. From 6a27781679430f932dae8575ca15abfc3b976e2e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 16 Dec 2015 11:41:52 +0200 Subject: [PATCH 4/8] Fix Not Allowed processing --- examples/web_classview1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/web_classview1.py b/examples/web_classview1.py index b4f9d1828b8..42840e63b45 100644 --- a/examples/web_classview1.py +++ b/examples/web_classview1.py @@ -24,7 +24,7 @@ def __await__(self): method = getattr(self, self.request.method, None) if method is None: allowed_methods = {m for m in ALL_METHODS if hasattr(self, m)} - return HTTPMethodNotAllowed(self.request.method, allowed_methods) + raise HTTPMethodNotAllowed(self.request.method, allowed_methods) resp = method().__await__() return resp @@ -70,7 +70,7 @@ async def index(request): async def init(loop): app = Application(loop=loop) app.router.add_route('GET', '/', index) - app.router.add_route('GET', '/get', View) + app.router.add_route('*', '/get', View) app.router.add_route('POST', '/post', View) app.router.add_route('PUT', '/put', View) app.router.add_route('DELETE', '/delete', View) From a66a92ea3afeeaf11b2d637dc123c8afae81f3c6 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 16 Dec 2015 13:15:10 +0200 Subject: [PATCH 5/8] Revert to GET from * in route --- examples/web_classview1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/web_classview1.py b/examples/web_classview1.py index 42840e63b45..8a4b48c1597 100644 --- a/examples/web_classview1.py +++ b/examples/web_classview1.py @@ -70,7 +70,7 @@ async def index(request): async def init(loop): app = Application(loop=loop) app.router.add_route('GET', '/', index) - app.router.add_route('*', '/get', View) + app.router.add_route('GET', '/get', View) app.router.add_route('POST', '/post', View) app.router.add_route('PUT', '/put', View) app.router.add_route('DELETE', '/delete', View) From b6e31ed1d8c35014819cc6887f2990087f27537c Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 17 Dec 2015 02:30:11 +0200 Subject: [PATCH 6/8] Implement web.View --- aiohttp/abc.py | 16 ++++++++++++++++ aiohttp/hdrs.py | 4 ++++ aiohttp/web_urldispatcher.py | 30 ++++++++++++++++++++++++++---- examples/web_classview1.py | 34 ++++++---------------------------- 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/aiohttp/abc.py b/aiohttp/abc.py index 360235c0f58..bfca672633b 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -21,3 +21,19 @@ def handler(self): @abstractmethod def route(self): """Return route for match info""" + + +class AbstractView(metaclass=ABCMeta): + + def __init__(self, request): + self._request = request + + @property + def request(self): + return self._request + + @asyncio.coroutine + @abstractmethod + def __iter__(self): + while False: + yield None diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py index 6ab09fee662..06c7936bbcc 100644 --- a/aiohttp/hdrs.py +++ b/aiohttp/hdrs.py @@ -12,6 +12,10 @@ METH_PUT = upstr('PUT') METH_TRACE = upstr('TRACE') +METH_ALL = {METH_CONNECT, METH_HEAD, METH_GET, METH_DELETE, + METH_OPTIONS, METH_PATCH, METH_POST, METH_PUT, METH_TRACE} + + ACCEPT = upstr('ACCEPT') ACCEPT_CHARSET = upstr('ACCEPT-CHARSET') ACCEPT_ENCODING = upstr('ACCEPT-ENCODING') diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 3770c640c43..c4faeb0b56b 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -13,7 +13,7 @@ from types import MappingProxyType from . import hdrs -from .abc import AbstractRouter, AbstractMatchInfo +from .abc import AbstractRouter, AbstractMatchInfo, AbstractView from .protocol import HttpVersion11 from .web_exceptions import HTTPMethodNotAllowed, HTTPNotFound, HTTPNotModified from .web_reqrep import StreamResponse @@ -21,7 +21,7 @@ __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', - 'Route', 'PlainRoute', 'DynamicRoute', 'StaticRoute') + 'Route', 'PlainRoute', 'DynamicRoute', 'StaticRoute', 'View') class UrlMappingMatchInfo(dict, AbstractMatchInfo): @@ -372,6 +372,23 @@ def __repr__(self): ', '.join(sorted(self._allowed_methods)))) +class View(AbstractView): + + @asyncio.coroutine + def __iter__(self): + if self.request.method not in hdrs.METH_ALL: + self._raise_allowed_methods() + method = getattr(self, self.request.method.lower(), None) + if method is None: + self._raise_allowed_methods() + resp = yield from method() + return resp + + def _raise_allowed_methods(self): + allowed_methods = {m for m in hdrs.METH_ALL if hasattr(self, m)} + raise HTTPMethodNotAllowed(self.request.method, allowed_methods) + + class RoutesView(Sized, Iterable, Container): __slots__ = '_urls' @@ -477,8 +494,13 @@ def add_route(self, method, path, handler, raise ValueError("path should be started with /") assert callable(handler), handler - if (not asyncio.iscoroutinefunction(handler) and - not inspect.isgeneratorfunction(handler)): + if asyncio.iscoroutinefunction(handler): + pass + elif inspect.isgeneratorfunction(handler): + pass + elif isinstance(handler, type) and issubclass(handler, AbstractView): + pass + else: handler = asyncio.coroutine(handler) method = upstr(method) diff --git a/examples/web_classview1.py b/examples/web_classview1.py index 8a4b48c1597..a8110181d1e 100644 --- a/examples/web_classview1.py +++ b/examples/web_classview1.py @@ -6,39 +6,19 @@ import asyncio import functools import json -from aiohttp import hdrs -from aiohttp.web import (json_response, Application, Response, - HTTPMethodNotAllowed) +from aiohttp.web import json_response, Application, Response, View -ALL_METHODS = {hdrs.METH_CONNECT, hdrs.METH_HEAD, hdrs.METH_GET, - hdrs.METH_DELETE, hdrs.METH_OPTIONS, hdrs.METH_PATCH, - hdrs.METH_POST, hdrs.METH_PUT, hdrs.METH_TRACE} +class MyView(View): - -class BaseView: - def __init__(self, request): - self.request = request - - def __await__(self): - method = getattr(self, self.request.method, None) - if method is None: - allowed_methods = {m for m in ALL_METHODS if hasattr(self, m)} - raise HTTPMethodNotAllowed(self.request.method, allowed_methods) - resp = method().__await__() - return resp - - -class View(BaseView): - - async def GET(self): + async def get(self): return json_response({ 'method': 'get', 'args': dict(self.request.GET), 'headers': dict(self.request.headers), }, dumps=functools.partial(json.dumps, indent=4)) - async def POST(self): + async def post(self): data = await self.request.post() return json_response({ 'method': 'post', @@ -70,10 +50,8 @@ async def index(request): async def init(loop): app = Application(loop=loop) app.router.add_route('GET', '/', index) - app.router.add_route('GET', '/get', View) - app.router.add_route('POST', '/post', View) - app.router.add_route('PUT', '/put', View) - app.router.add_route('DELETE', '/delete', View) + app.router.add_route('GET', '/get', MyView) + app.router.add_route('POST', '/post', MyView) handler = app.make_handler() srv = await loop.create_server(handler, '127.0.0.1', 8080) From 9768c464a14b9b9e3226af46a50be7a1d53efe87 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 17 Dec 2015 03:21:52 +0200 Subject: [PATCH 7/8] Improve coverage --- aiohttp/abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/abc.py b/aiohttp/abc.py index bfca672633b..1296ea1be5c 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -35,5 +35,5 @@ def request(self): @asyncio.coroutine @abstractmethod def __iter__(self): - while False: + while False: # pragma: no cover yield None From d6e6c5caa26469b101333df5a901a94f3fe532f3 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 17 Dec 2015 18:29:11 +0200 Subject: [PATCH 8/8] Add tests for class based views --- tests/test_classbasedview.py | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test_classbasedview.py diff --git a/tests/test_classbasedview.py b/tests/test_classbasedview.py new file mode 100644 index 00000000000..a0613e16819 --- /dev/null +++ b/tests/test_classbasedview.py @@ -0,0 +1,57 @@ +import asyncio +import pytest + +from aiohttp import web +from aiohttp.web_urldispatcher import View +from unittest import mock + + +def test_ctor(): + request = mock.Mock() + view = View(request) + assert view.request is request + + +@pytest.mark.run_loop +def test_render_ok(): + resp = web.Response(text='OK') + + class MyView(View): + @asyncio.coroutine + def get(self): + return resp + + request = mock.Mock() + request.method = 'GET' + resp2 = yield from MyView(request) + assert resp is resp2 + + +@pytest.mark.run_loop +def test_render_unknown_method(): + + class MyView(View): + @asyncio.coroutine + def get(self): + return web.Response(text='OK') + + request = mock.Mock() + request.method = 'UNKNOWN' + with pytest.raises(web.HTTPMethodNotAllowed) as ctx: + yield from MyView(request) + assert ctx.value.status == 405 + + +@pytest.mark.run_loop +def test_render_unsupported_method(): + + class MyView(View): + @asyncio.coroutine + def get(self): + return web.Response(text='OK') + + request = mock.Mock() + request.method = 'POST' + with pytest.raises(web.HTTPMethodNotAllowed) as ctx: + yield from MyView(request) + assert ctx.value.status == 405