Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signals v2 #562

Merged
merged 31 commits into from
Oct 16, 2015
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
654a806
Initial signal implementation. Tests and documentation to follow.
alexdutton Jul 9, 2015
ac75361
Wrap iscoroutinefunction check in 'if __debug__', so people can optim…
alexdutton Jul 20, 2015
fefd2ed
Rename AsyncSignal to CoroutineSignal for clarity of purpose
alexdutton Jul 20, 2015
d45ff67
Add base class for signals
alexdutton Jul 20, 2015
cf29660
Add signal tests
alexdutton Jul 20, 2015
98c418a
Documentation!
alexdutton Jul 21, 2015
4d8b509
Point at FunctionSignal, not Signal in `on_response_start` docs
alexdutton Jul 21, 2015
9bedbbb
Merge remote-tracking branch 'upstream/master' into signals-v2
alexdutton Sep 25, 2015
f04fbb3
Remove FunctionSignals in light of #525.
alexdutton Sep 25, 2015
5fb868b
Move on_response_start firing to `prepare()` and treat it as a coroutine
alexdutton Sep 25, 2015
cc2efbd
Raise TypeError on non-coroutine functions, to match signature mismat…
alexdutton Sep 25, 2015
9170057
Working tests again.
alexdutton Sep 25, 2015
5825da3
Signal now based on list; still does signature checking
alexdutton Sep 28, 2015
dea0a1e
Merge remote-tracking branch 'upstream/master' into signals-v2
alexdutton Sep 28, 2015
f5b98ac
Drop requirement for signal receivers to be coroutines (but they stil…
alexdutton Sep 28, 2015
322f650
Fix variable name in signature check call
alexdutton Sep 28, 2015
a0f10f7
Merge branch 'signals-v2' of https://github.com/alexsdutton/aiohttp i…
asvetlov Oct 11, 2015
dbc8393
Drop signal signature check
asvetlov Oct 11, 2015
b037e2b
Add more tests
asvetlov Oct 11, 2015
15d815e
Allow using positional args to Signal.send
asvetlov Oct 11, 2015
74413d4
Fix failed test
asvetlov Oct 11, 2015
67414c8
Update docs
asvetlov Oct 11, 2015
02c44a4
Merge branch 'master' into signals-v2
asvetlov Oct 12, 2015
6cd8a44
Convert signal tests to pytest usage
asvetlov Oct 12, 2015
1a9c0a7
Fix tests
asvetlov Oct 13, 2015
0089a65
Properly mock coroutine
asvetlov Oct 13, 2015
053d184
Fix signals test
asvetlov Oct 13, 2015
73ad8fb
Merge branch 'master' into signals-v2
asvetlov Oct 13, 2015
602b19c
Merge branch 'master' into signals-v2
asvetlov Oct 14, 2015
9939f94
Fix failed test
asvetlov Oct 14, 2015
0c32f47
Fix next test
asvetlov Oct 14, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions aiohttp/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import asyncio


class Signal(list):
"""
Coroutine-based signal implementation

To connect a callback to a signal, use any list method. If wish to pass
additional arguments to your callback, use :meth:`functools.partial`.

Signals are fired using the :meth:`send` coroutine, which takes named
arguments.
"""

@asyncio.coroutine
def send(self, **kwargs):
"""
Sends data to all registered receivers.
"""
for receiver in self:
res = receiver(**kwargs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to ensure that all the receivers in Signal are callable? Otherwise it won't be cool to get some TypeError: 'int' object is not callable from the deepest aiohttp internals without any pointers about what the signal that was and where it came from.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial version from @alexsdutton had signature check. But I quite unhappy with it.
I prefer adding good checker later but keeping no checks at all in first implementation.

if asyncio.iscoroutine(res) or isinstance(res, asyncio.Future):
yield from res

def copy(self):
raise NotImplementedError("copy() is forbidden")

def sort(self):
raise NotImplementedError("sort() is forbidden")
3 changes: 3 additions & 0 deletions aiohttp/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .web_urldispatcher import * # noqa
from .web_ws import * # noqa
from .protocol import HttpVersion # noqa
from .signals import Signal


import asyncio
Expand Down Expand Up @@ -196,6 +197,8 @@ def __init__(self, *, logger=web_logger, loop=None,
assert asyncio.iscoroutinefunction(factory), factory
self._middlewares = list(middlewares)

self.on_response_prepare = Signal()

@property
def router(self):
return self._router
Expand Down
2 changes: 2 additions & 0 deletions aiohttp/web_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,8 @@ def prepare(self, request):
resp_impl = self._start_pre_check(request)
if resp_impl is not None:
return resp_impl
yield from request.app.on_response_prepare.send(request=request,
response=self)

return self._start(request)

Expand Down
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ aiohttp.protocol module
:undoc-members:
:show-inheritance:

aiohttp.signals module
----------------------

.. automodule:: aiohttp.signals
:members:
:undoc-members:
:show-inheritance:

aiohttp.streams module
----------------------

Expand Down
10 changes: 9 additions & 1 deletion docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,8 @@ StreamResponse
response answers.

Send *HTTP header*. You should not change any header data after
calling this method.
calling this method, except through
:attr:`Application.on_response_start` signal callbacks.

.. deprecated:: 0.18

Expand Down Expand Up @@ -920,6 +921,13 @@ arbitrary properties for later access from

:ref:`event loop<asyncio-event-loop>` used for processing HTTP requests.

.. attribute:: on_response_start

A :class:`~aiohttp.signals.FunctionSignal` that is fired at the beginning
of :meth:`StreamResponse.start` with parameters ``request`` and
``response``. It can be used, for example, to add custom headers to each
response before sending.

.. method:: make_handler(**kwargs)

Creates HTTP protocol factory for handling requests.
Expand Down
95 changes: 95 additions & 0 deletions tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import asyncio
import unittest
from unittest import mock
from aiohttp.multidict import CIMultiDict
from aiohttp.signals import Signal
from aiohttp.web import Application
from aiohttp.web import Request, Response
from aiohttp.protocol import HttpVersion11
from aiohttp.protocol import RawRequestMessage


class TestSignals(unittest.TestCase):
def setUp(self):
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(None)

def tearDown(self):
self.loop.close()

def make_request(self, method, path, headers=CIMultiDict(), app=None):
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
return self.request_from_message(message, app)

def request_from_message(self, message, app=None):
self.app = app if app is not None else mock.Mock()
self.payload = mock.Mock()
self.transport = mock.Mock()
self.reader = mock.Mock()
self.writer = mock.Mock()
req = Request(self.app, message, self.payload,
self.transport, self.reader, self.writer)
return req

def test_add_response_prepare_signal_handler(self):
callback = asyncio.coroutine(lambda request, response: None)
app = Application(loop=self.loop)
app.on_response_prepare.append(callback)

def test_add_signal_handler_not_a_callable(self):
callback = True
app = Application(loop=self.loop)
app.on_response_prepare.append(callback)
with self.assertRaises(TypeError):
app.on_response_prepare(None, None)

def test_function_signal_dispatch(self):
signal = Signal()
kwargs = {'foo': 1, 'bar': 2}

callback_mock = mock.Mock()
callback = asyncio.coroutine(callback_mock)

signal.append(callback)

self.loop.run_until_complete(signal.send(**kwargs))
callback_mock.assert_called_once_with(**kwargs)

def test_response_prepare(self):
callback = mock.Mock()

app = Application(loop=self.loop)
app.on_response_prepare.append(asyncio.coroutine(callback))

request = self.make_request('GET', '/', app=app)
response = Response(body=b'')
self.loop.run_until_complete(response.prepare(request))

callback.assert_called_once_with(request=request,
response=response)

def test_non_coroutine(self):
signal = Signal()
kwargs = {'foo': 1, 'bar': 2}

callback = mock.Mock()

signal.append(callback)

self.loop.run_until_complete(signal.send(**kwargs))
callback.assert_called_once_with(**kwargs)

def test_copy_forbidden(self):
signal = Signal()
with self.assertRaises(NotImplementedError):
signal.copy()

def test_sort_forbidden(self):
l1 = lambda: None
l2 = lambda: None
l3 = lambda: None
signal = Signal([l1, l2, l3])
with self.assertRaises(NotImplementedError):
signal.sort()
self.assertEqual(signal, [l1, l2, l3])
3 changes: 2 additions & 1 deletion tests/test_web_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from aiohttp.web import Request
from aiohttp.protocol import RawRequestMessage, HttpVersion11

from aiohttp import web
from aiohttp import signals, web


class TestHTTPExceptions(unittest.TestCase):
Expand All @@ -32,6 +32,7 @@ def append(self, data):

def make_request(self, method='GET', path='/', headers=CIMultiDict()):
self.app = mock.Mock()
self.app.on_response_prepare = signals.Signal()
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
req = Request(self.app, message, self.payload,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_web_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import unittest
from unittest import mock
from aiohttp.signals import Signal
from aiohttp.web import Request
from aiohttp.multidict import MultiDict, CIMultiDict
from aiohttp.protocol import HttpVersion
Expand All @@ -23,6 +24,7 @@ def make_request(self, method, path, headers=CIMultiDict(), *,
if version < HttpVersion(1, 1):
closing = True
self.app = mock.Mock()
self.app.on_response_prepare = Signal()
message = RawRequestMessage(method, path, version, headers, closing,
False)
self.payload = mock.Mock()
Expand Down
14 changes: 13 additions & 1 deletion tests/test_web_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import datetime
import unittest
from unittest import mock
from aiohttp import hdrs
from aiohttp import hdrs, signals
from aiohttp.multidict import CIMultiDict
from aiohttp.web import ContentCoding, Request, StreamResponse, Response
from aiohttp.protocol import HttpVersion, HttpVersion11, HttpVersion10
Expand All @@ -26,6 +26,7 @@ def make_request(self, method, path, headers=CIMultiDict(),

def request_from_message(self, message):
self.app = mock.Mock()
self.app.on_response_prepare = signals.Signal()
self.payload = mock.Mock()
self.transport = mock.Mock()
self.reader = mock.Mock()
Expand Down Expand Up @@ -514,6 +515,16 @@ def test_start_twice(self, ResponseImpl):
impl2 = resp.start(req)
self.assertIs(impl1, impl2)

def test_prepare_calls_signal(self):
req = self.make_request('GET', '/')
resp = StreamResponse()

sig = mock.Mock()
self.app.on_response_prepare.append(sig)
self.loop.run_until_complete(resp.prepare(req))

sig.assert_called_with(request=req, response=resp)


class TestResponse(unittest.TestCase):

Expand All @@ -526,6 +537,7 @@ def tearDown(self):

def make_request(self, method, path, headers=CIMultiDict()):
self.app = mock.Mock()
self.app.on_response_prepare = signals.Signal()
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
self.payload = mock.Mock()
Expand Down
3 changes: 2 additions & 1 deletion tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from aiohttp.web import (
MsgType, Request, WebSocketResponse, HTTPMethodNotAllowed, HTTPBadRequest)
from aiohttp.protocol import RawRequestMessage, HttpVersion11
from aiohttp import errors, websocket
from aiohttp import errors, signals, websocket


class TestWebWebSocket(unittest.TestCase):
Expand Down Expand Up @@ -37,6 +37,7 @@ def make_request(self, method, path, headers=None, protocols=False):
self.reader = mock.Mock()
self.writer = mock.Mock()
self.app.loop = self.loop
self.app.on_response_prepare = signals.Signal()
req = Request(self.app, message, self.payload,
self.transport, self.reader, self.writer)
return req
Expand Down