Skip to content

Commit

Permalink
Add context to Response controller
Browse files Browse the repository at this point in the history
  • Loading branch information
sametmax committed Jul 1, 2016
1 parent d392546 commit 9fdae78
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 32 deletions.
17 changes: 16 additions & 1 deletion src/tygs/components.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio

from functools import partial, wraps
from textwrap import dedent

import jinja2

Expand All @@ -10,6 +11,7 @@

from .utils import ensure_coroutine, HTTP_VERBS
from .http.server import HttpRequestController, Router
from .exceptions import HttpResponseControllerError


class Component:
Expand Down Expand Up @@ -62,7 +64,19 @@ def render_to_string(self, template, context):
return template.render(context)

def render_to_response_dict(self, response):
body = self.render_to_string(response.template_name, response.data)

try:
template_name = response.context['template_name']

except KeyError:
raise HttpResponseControllerError(dedent("""
"{!r}" context doesn't contain a template name. When you
call res.template(), the template name is stored in
res.context['template_name']. Make sure nothing
overrides this key after it.
""".format(response)))

body = self.render_to_string(template_name, response._renderer_data)
body = body.encode(response.charset)

return {'status': response.status,
Expand Down Expand Up @@ -113,6 +127,7 @@ def __init__(self, *args, tygs_app, **kwargs):

async def _tygs_request_from_message(self, message, payload):
app = self._app

aiothttp_request = Request(
app, message, payload,
self.transport, self.reader, self.writer,
Expand Down
4 changes: 4 additions & 0 deletions src/tygs/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ class TygsError(Exception):

class HttpRequestControllerError(TygsError):
pass


class HttpResponseControllerError(TygsError):
pass
66 changes: 47 additions & 19 deletions src/tygs/http/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@

from werkzeug.routing import Map, Rule

from tygs.exceptions import HttpRequestControllerError
from tygs.exceptions import (HttpRequestControllerError,
HttpResponseControllerError)
from tygs.utils import HTTP_VERBS, removable_property


# TODO: move this function and other renderers to a dedicated module
def text_renderer(response):
body = response.data['__text__'].encode(response.charset)
body = response._renderer_data.encode(response.charset)

return {'status': response.status,
'reason': response.reason,
Expand All @@ -26,6 +27,17 @@ def text_renderer(response):
}


# TODO: allow the user to configure a default renderer for when no renderer
# is set instead of getting this error. We'll need to update the exception
# message though
def no_renderer(response):
raise HttpResponseControllerError(dedent("""
No renderer set on "{!r}". Please set manually a renderer on it or
call one of the shortcut methods to do so (res.text(),
res.template(), res.json(), etc).
""".format(response)))


class HttpRequestController:

# TODO: decouple aiothttp_request from HttpRequestController
Expand Down Expand Up @@ -64,7 +76,7 @@ def __getitem__(self, name):
except KeyError:
pass

return super().__getitem__(self, name)
return super().__getitem__(name)

def __iter__(self):
for x in self.url_query:
Expand Down Expand Up @@ -92,21 +104,25 @@ def __len__(self, name):

def __getattr__(self, name):

if name == "GET":
verb = name.upper()
if verb == "GET":
raise HttpRequestControllerError(dedent("""
There is no "GET{0}" attribute. If you are looking for the
There is no "GET" attribute. If you are looking for the
data passed as the URL query sting (usually $_GET, .GET, etc.),
use the "query_args" attribute.
"""))

if name in HTTP_VERBS:
if verb in HTTP_VERBS:
raise HttpRequestControllerError(dedent("""
There is no "{0}" attribute. If you are looking for the
request body (usually called $_POST, $_GET, etc.), use the
"body" attribute. It works with all HTTP verbs and not
just "{0}".
""".format(name)))

# Do not try super().__getattr__ since the parent doesn't define it.
raise object.__getattribute__(self, name)

@reify
def url_query(self):
return self._aiohttp_request.GET
Expand Down Expand Up @@ -140,37 +156,49 @@ class HttpResponseController:

def __init__(self, request):
self.request = request
self.renderer = None
self.template_name = None
self._renderer = no_renderer
self.template_engine = request.app.components.get('templates', None)
self.data = {}
self._renderer_data = None
self.context = {"req": request, "res": self}

# TODO : set reason automatically when you set status
# TODO: create a status object with embeded reason
# TODO: create a status NameSpace with embeded reason and code
self.status = 200
self.content_type = "text/html"
self.reason = "OK"
self.charset = "utf-8"
self.headers = {}

def template(self, template, context=None):
def __repr__(self):
req = self.request
return "<{} {} {!r} >".format(self.__class__.__name__,
req.method, req.url_path)

# TODO: allow template engine to be passed here as a parameter, but
# also be retrieved from the app configuration. And remove it as an
# attribute of the HttpResponseController.
def template(self, template, data=None):
"""
Registers context variables and template name.
Registers data variables and template name.
This method does not actually render templates.
"""
context = context or {}

self.context['template_name'] = template

self._renderer_data = {'context': self.context}
self._renderer_data.update(data)

# TODO make template engine pluggable
self.renderer = self.template_engine.render_to_response_dict
self.template_name = template
self.data.update(context)
self._renderer = self.template_engine.render_to_response_dict

return self

def text(self, message):
self.data['__text__'] = str(message)
self.renderer = text_renderer
self._renderer_data = str(message)
self._renderer = text_renderer

def render_response(self):
return self.renderer(self)
return self._renderer(self)

def _build_aiohttp_response(self):
return Response(**self.render_response())
Expand Down
12 changes: 12 additions & 0 deletions src/tygs/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from unittest.mock import Mock

from requests.models import RequestEncodingMixin

from aiohttp.streams import StreamReader


class AsyncMock(Mock):

Expand All @@ -14,3 +18,11 @@ async def __call__(self, *args, **kwargs):

def __await__(self):
return self().__await__()


def aiohttp_payload(data, encoding="utf8"):
payload = RequestEncodingMixin._encode_params(data).encode(encoding)
stream = StreamReader()
stream.feed_data(payload)
stream.feed_eof()
return stream
25 changes: 22 additions & 3 deletions tests/test_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from tygs import components, app
from tygs.components import AioHttpRequestHandlerAdapter
from tygs.test_utils import AsyncMock
from tygs.test_utils import AsyncMock, aiohttp_payload
from tygs.http.server import (Router, HttpResponseController,
HttpRequestController)
from tygs.app import App
Expand Down Expand Up @@ -102,7 +102,7 @@ def test_jinja2_renderer_render_to_response_dict(app):
render = MagicMock()
render.return_value = 'Hey, I’m a body!'
resp.template_engine.render_to_string = render
resp.template_name = 'test_resp.html'
resp.context['template_name'] = 'test_resp.html'

rendered = resp.template_engine.render_to_response_dict(resp)

Expand All @@ -113,7 +113,7 @@ def test_jinja2_renderer_render_to_response_dict(app):
'charset': 'utf-8',
'headers': {},
'body': 'Hey, I’m a body!'.encode()}
render.assert_called_once_with('test_resp.html', {})
render.assert_called_once_with('test_resp.html', None)


@pytest.mark.asyncio
Expand Down Expand Up @@ -335,6 +335,25 @@ async def toto(req, res):
assert handleradapter.log_access.call_count == 0


@pytest.mark.asyncio
async def test_request_body_loading_no_lazy(handleradapter, aiohttpmsg):

handleradapter._write_response_to_client = AsyncMock()

async def toto(req, res):
assert req.body['cheese'] == 'Stilton'
return res

toto = handleradapter.tygs_app.components['http'].post('/toto')(toto)
payload = aiohttp_payload({'cheese': 'Stilton'})

message = aiohttpmsg('POST', '/toto',
headers={'Content-Type':
'application/x-www-form-urlencoded'})

await handleradapter.handle_request(message, payload)


@pytest.mark.asyncio
async def test_requesthandleradapter_write_response_to_client(handleradapter,
webapp):
Expand Down
20 changes: 11 additions & 9 deletions tests/test_http_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ def test_httpresponse_controller_init(app):
assert httpresponse.content_type == 'text/html'
assert httpresponse.charset == 'utf-8'
assert httpresponse.headers == {}
assert httpresponse.data == {}


def test_httpresponse_controller_template(webapp):
Expand All @@ -46,23 +45,26 @@ def test_httpresponse_controller_template(webapp):

httpresponse.template('template_name', {'foo': 'bar'})

assert httpresponse.template_name == 'template_name'
assert httpresponse.data == {'foo': 'bar'}
assert httpresponse.context['template_name'] == 'template_name'
context = httpresponse._renderer_data.pop('context')
assert "res" in context and "req" in context
assert httpresponse._renderer_data == {'foo': 'bar'}

httpresponse.template('template_name2', {'foo2': 'bar2'})

assert httpresponse.template_name == 'template_name2'
assert httpresponse.data == {'foo': 'bar', 'foo2': 'bar2'}
context = httpresponse._renderer_data.pop('context')
assert "res" in context and "req" in context
assert httpresponse.context['template_name'] == 'template_name2'
assert httpresponse._renderer_data == {'foo2': 'bar2'}


def test_httpresponse_controller_render_response(webapp):
request = MagicMock()
request.app = webapp
httpresponse = server.HttpResponseController(request)
httpresponse.renderer = Mock()
httpresponse._renderer = Mock()

httpresponse.render_response()
httpresponse.renderer.assert_called_once_with(httpresponse)
httpresponse._renderer.assert_called_once_with(httpresponse)


def test_httpresponse_build_aiohttp_reponse(webapp):
Expand Down Expand Up @@ -101,7 +103,7 @@ async def test_request_params(queued_webapp):

@http.get('/')
def index_controller(req, res):
pass
return res.text('test')

await queued_webapp.async_ready()

Expand Down

0 comments on commit 9fdae78

Please sign in to comment.