Skip to content

Commit

Permalink
Merge pull request #684 from KeepSafe/classbasedview
Browse files Browse the repository at this point in the history
Classbasedview
  • Loading branch information
asvetlov committed Dec 23, 2015
2 parents 62e8629 + d6e6c5c commit b06021f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 4 deletions.
16 changes: 16 additions & 0 deletions aiohttp/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: # pragma: no cover
yield None
4 changes: 4 additions & 0 deletions aiohttp/hdrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
30 changes: 26 additions & 4 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
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
from .multidict import upstr


__all__ = ('UrlDispatcher', 'UrlMappingMatchInfo',
'Route', 'PlainRoute', 'DynamicRoute', 'StaticRoute')
'Route', 'PlainRoute', 'DynamicRoute', 'StaticRoute', 'View')


class UrlMappingMatchInfo(dict, AbstractMatchInfo):
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
67 changes: 67 additions & 0 deletions examples/web_classview1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Example for aiohttp.web class based views
"""


import asyncio
import functools
import json
from aiohttp.web import json_response, Application, Response, View


class MyView(View):

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):
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):
txt = """
<html>
<head>
<title>Class based view example</title>
</head>
<body>
<h1>Class based view example</h1>
<ul>
<li><a href="/">/</a> This page
<li><a href="/get">/get</a> Returns GET data.
<li><a href="/post">/post</a> Returns POST data.
</ul>
</body>
</html>
"""
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', MyView)
app.router.add_route('POST', '/post', MyView)

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())
57 changes: 57 additions & 0 deletions tests/test_classbasedview.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b06021f

Please sign in to comment.