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
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Convert signal tests to pytest usage
asvetlov committed Oct 12, 2015
commit 6cd8a442938f866d93a44490275c291f81700d5b
65 changes: 54 additions & 11 deletions aiohttp/signals.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,71 @@
import asyncio
from itertools import count


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

@asyncio.coroutine
def _send(self, *args, **kwargs):
for receiver in self:
res = receiver(*args, **kwargs)
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")


class Signal(BaseSignal):
"""Coroutine-based signal implementation.

To connect a callback to a signal, use any list method.

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

def __init__(self, app):
super().__init__()
self._app = app
klass = self.__class__
self._name = klass.__module__ + ':' + klass.__qualname__
self._pre = app.on_pre_signal
self._post = app.on_post_signal

@asyncio.coroutine
def send(self, *args, **kwargs):
"""
Sends data to all registered receivers.
"""
for receiver in self:
res = receiver(*args, **kwargs)
if asyncio.iscoroutine(res) or isinstance(res, asyncio.Future):
yield from res
ordinal = None
debug = self._app._debug
if debug:
ordinal = self._pre.ordinal()
yield from self._pre.send(ordinal, self._name, *args, **kwargs)
yield from self._send(*args, **kwargs)
if debug:
yield from self._post.send(ordinal, self._name, *args, **kwargs)

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

def sort(self):
raise NotImplementedError("sort() is forbidden")
class DebugSignal(BaseSignal):

@asyncio.coroutine
def send(self, ordinal, name, *args, **kwargs):
yield from self._send(ordinal, name, *args, **kwargs)


class PreSignal(DebugSignal):

def __init__(self):
super().__init__()
self._counter = count(1)

def ordinal(self):
return next(self._counter)


class PostSignal(DebugSignal):
pass
21 changes: 18 additions & 3 deletions aiohttp/web.py
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
from .web_urldispatcher import * # noqa
from .web_ws import * # noqa
from .protocol import HttpVersion # noqa
from .signals import Signal
from .signals import Signal, PreSignal, PostSignal


import asyncio
@@ -180,13 +180,14 @@ class Application(dict):

def __init__(self, *, logger=web_logger, loop=None,
router=None, handler_factory=RequestHandlerFactory,
middlewares=()):
middlewares=(), debug=False):
if loop is None:
loop = asyncio.get_event_loop()
if router is None:
router = UrlDispatcher()
assert isinstance(router, AbstractRouter), router

self._debug = debug
self._router = router
self._handler_factory = handler_factory
self._finish_callbacks = []
@@ -197,12 +198,26 @@ def __init__(self, *, logger=web_logger, loop=None,
assert asyncio.iscoroutinefunction(factory), factory
self._middlewares = list(middlewares)

self._on_response_prepare = Signal()
self._on_pre_signal = PreSignal()
self._on_post_signal = PostSignal()
self._on_response_prepare = Signal(self)

@property
def debug(self):
return self._debug

@property
def on_response_prepare(self):
return self._on_response_prepare

@property
def on_pre_signal(self):
return self._on_pre_signal

@property
def on_post_signal(self):
return self._on_post_signal

@property
def router(self):
return self._router
154 changes: 79 additions & 75 deletions tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import asyncio
import unittest
from unittest import mock
from aiohttp.multidict import CIMultiDict
from aiohttp.signals import Signal
@@ -8,101 +7,106 @@
from aiohttp.protocol import HttpVersion11
from aiohttp.protocol import RawRequestMessage

import pytest

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

def tearDown(self):
self.loop.close()
@pytest.fixture
def app(loop):
return Application(loop=loop)

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 make_request(app, method, path, headers=CIMultiDict()):
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
return request_from_message(message, app)

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 request_from_message(message, app):
payload = mock.Mock()
transport = mock.Mock()
reader = mock.Mock()
writer = mock.Mock()
req = Request(app, message, payload,
transport, reader, writer)
return req

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

callback_mock = mock.Mock()
callback = asyncio.coroutine(callback_mock)
def test_add_response_prepare_signal_handler(loop, app):
callback = asyncio.coroutine(lambda request, response: None)
app.on_response_prepare.append(callback)

signal.append(callback)

self.loop.run_until_complete(signal.send(**kwargs))
callback_mock.assert_called_once_with(**kwargs)
def test_add_signal_handler_not_a_callable(loop, app):
callback = True
app.on_response_prepare.append(callback)
with pytest.raises(TypeError):
app.on_response_prepare(None, None)

def test_function_signal_dispatch2(self):
signal = Signal()
args = {'a', 'b'}
kwargs = {'foo': 1, 'bar': 2}

callback_mock = mock.Mock()
callback = asyncio.coroutine(callback_mock)
def test_function_signal_dispatch(loop, app):
signal = Signal(app)
kwargs = {'foo': 1, 'bar': 2}

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

self.loop.run_until_complete(signal.send(*args, **kwargs))
callback_mock.assert_called_once_with(*args, **kwargs)
signal.append(callback)

def test_response_prepare(self):
callback = mock.Mock()
loop.run_until_complete(signal.send(**kwargs))
callback_mock.assert_called_once_with(**kwargs)

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))
def test_function_signal_dispatch2(loop, app):
signal = Signal(app)
args = {'a', 'b'}
kwargs = {'foo': 1, 'bar': 2}

callback.assert_called_once_with(request=request,
response=response)
callback_mock = mock.Mock()
callback = asyncio.coroutine(callback_mock)

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

callback = mock.Mock()
loop.run_until_complete(signal.send(*args, **kwargs))
callback_mock.assert_called_once_with(*args, **kwargs)

signal.append(callback)

self.loop.run_until_complete(signal.send(**kwargs))
callback.assert_called_once_with(**kwargs)
def test_response_prepare(loop, app):
callback = mock.Mock()

def test_copy_forbidden(self):
signal = Signal()
with self.assertRaises(NotImplementedError):
signal.copy()
app.on_response_prepare.append(asyncio.coroutine(callback))

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])
request = make_request(app, 'GET', '/')
response = Response(body=b'')
loop.run_until_complete(response.prepare(request))

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


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

callback = mock.Mock()

signal.append(callback)

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


def test_copy_forbidden(app):
signal = Signal(app)
with pytest.raises(NotImplementedError):
signal.copy()


def test_sort_forbidden(app):
l1 = lambda: None
l2 = lambda: None
l3 = lambda: None
signal = Signal(app)
signal.extend([l1, l2, l3])
with pytest.raises(NotImplementedError):
signal.sort()
assert 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
@@ -32,7 +32,8 @@ def append(self, data):

def make_request(self, method='GET', path='/', headers=CIMultiDict()):
self.app = mock.Mock()
self.app.on_response_prepare = signals.Signal()
self.app._debug = False
self.app.on_response_prepare = signals.Signal(self.app)
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
req = Request(self.app, message, self.payload,
3 changes: 2 additions & 1 deletion tests/test_web_request.py
Original file line number Diff line number Diff line change
@@ -24,7 +24,8 @@ 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()
self.app._debug = False
self.app.on_response_prepare = Signal(self.app)
message = RawRequestMessage(method, path, version, headers, closing,
False)
self.payload = mock.Mock()
6 changes: 4 additions & 2 deletions tests/test_web_response.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,8 @@ 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.app._debug = False
self.app.on_response_prepare = signals.Signal(self.app)
self.payload = mock.Mock()
self.transport = mock.Mock()
self.reader = mock.Mock()
@@ -537,7 +538,8 @@ def tearDown(self):

def make_request(self, method, path, headers=CIMultiDict()):
self.app = mock.Mock()
self.app.on_response_prepare = signals.Signal()
self.app._debug = False
self.app.on_response_prepare = signals.Signal(self.app)
message = RawRequestMessage(method, path, HttpVersion11, headers,
False, False)
self.payload = mock.Mock()
3 changes: 2 additions & 1 deletion tests/test_web_websocket.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@ def tearDown(self):

def make_request(self, method, path, headers=None, protocols=False):
self.app = mock.Mock()
self.app._debug = False
if headers is None:
headers = CIMultiDict(
{'HOST': 'server.example.com',
@@ -37,7 +38,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()
self.app.on_response_prepare = signals.Signal(self.app)
req = Request(self.app, message, self.payload,
self.transport, self.reader, self.writer)
return req