Skip to content

Commit 6dd0eb7

Browse files
committed
Merge pull request #562 from KeepSafe/signals-v2
Signals v2
2 parents e2eceae + 0c32f47 commit 6dd0eb7

File tree

11 files changed

+288
-5
lines changed

11 files changed

+288
-5
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ vtest: flake .develop
2525
cov cover coverage:
2626
tox
2727

28-
cov-dev: flake develop
28+
cov-dev: develop
2929
@coverage erase
3030
@coverage run -m pytest -s tests
3131
@mv .coverage .coverage.accel

aiohttp/signals.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import asyncio
2+
from itertools import count
3+
4+
5+
class BaseSignal(list):
6+
7+
@asyncio.coroutine
8+
def _send(self, *args, **kwargs):
9+
for receiver in self:
10+
res = receiver(*args, **kwargs)
11+
if asyncio.iscoroutine(res) or isinstance(res, asyncio.Future):
12+
yield from res
13+
14+
def copy(self):
15+
raise NotImplementedError("copy() is forbidden")
16+
17+
def sort(self):
18+
raise NotImplementedError("sort() is forbidden")
19+
20+
21+
class Signal(BaseSignal):
22+
"""Coroutine-based signal implementation.
23+
24+
To connect a callback to a signal, use any list method.
25+
26+
Signals are fired using the :meth:`send` coroutine, which takes named
27+
arguments.
28+
"""
29+
30+
def __init__(self, app):
31+
super().__init__()
32+
self._app = app
33+
klass = self.__class__
34+
self._name = klass.__module__ + ':' + klass.__qualname__
35+
self._pre = app.on_pre_signal
36+
self._post = app.on_post_signal
37+
38+
@asyncio.coroutine
39+
def send(self, *args, **kwargs):
40+
"""
41+
Sends data to all registered receivers.
42+
"""
43+
ordinal = None
44+
debug = self._app._debug
45+
if debug:
46+
ordinal = self._pre.ordinal()
47+
yield from self._pre.send(ordinal, self._name, *args, **kwargs)
48+
yield from self._send(*args, **kwargs)
49+
if debug:
50+
yield from self._post.send(ordinal, self._name, *args, **kwargs)
51+
52+
53+
class DebugSignal(BaseSignal):
54+
55+
@asyncio.coroutine
56+
def send(self, ordinal, name, *args, **kwargs):
57+
yield from self._send(ordinal, name, *args, **kwargs)
58+
59+
60+
class PreSignal(DebugSignal):
61+
62+
def __init__(self):
63+
super().__init__()
64+
self._counter = count(1)
65+
66+
def ordinal(self):
67+
return next(self._counter)
68+
69+
70+
class PostSignal(DebugSignal):
71+
pass

aiohttp/web.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .web_urldispatcher import * # noqa
88
from .web_ws import * # noqa
99
from .protocol import HttpVersion # noqa
10+
from .signals import Signal, PreSignal, PostSignal
1011

1112

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

180181
def __init__(self, *, logger=web_logger, loop=None,
181182
router=None, handler_factory=RequestHandlerFactory,
182-
middlewares=()):
183+
middlewares=(), debug=False):
183184
if loop is None:
184185
loop = asyncio.get_event_loop()
185186
if router is None:
186187
router = UrlDispatcher()
187188
assert isinstance(router, AbstractRouter), router
188189

190+
self._debug = debug
189191
self._router = router
190192
self._handler_factory = handler_factory
191193
self._finish_callbacks = []
@@ -196,6 +198,26 @@ def __init__(self, *, logger=web_logger, loop=None,
196198
assert asyncio.iscoroutinefunction(factory), factory
197199
self._middlewares = list(middlewares)
198200

201+
self._on_pre_signal = PreSignal()
202+
self._on_post_signal = PostSignal()
203+
self._on_response_prepare = Signal(self)
204+
205+
@property
206+
def debug(self):
207+
return self._debug
208+
209+
@property
210+
def on_response_prepare(self):
211+
return self._on_response_prepare
212+
213+
@property
214+
def on_pre_signal(self):
215+
return self._on_pre_signal
216+
217+
@property
218+
def on_post_signal(self):
219+
return self._on_post_signal
220+
199221
@property
200222
def router(self):
201223
return self._router

aiohttp/web_reqrep.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,8 @@ def prepare(self, request):
646646
resp_impl = self._start_pre_check(request)
647647
if resp_impl is not None:
648648
return resp_impl
649+
yield from request.app.on_response_prepare.send(request=request,
650+
response=self)
649651

650652
return self._start(request)
651653

docs/api.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ aiohttp.protocol module
4949
:undoc-members:
5050
:show-inheritance:
5151

52+
aiohttp.signals module
53+
----------------------
54+
55+
.. automodule:: aiohttp.signals
56+
:members:
57+
:undoc-members:
58+
:show-inheritance:
59+
5260
aiohttp.streams module
5361
----------------------
5462

docs/web_reference.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,10 @@ StreamResponse
560560

561561
Use :meth:`prepare` instead.
562562

563+
.. warning:: The method doesn't call
564+
:attr:`web.Application.on_response_prepare` signal, use
565+
:meth:`prepare` instead.
566+
563567
.. coroutinemethod:: prepare(request)
564568

565569
:param aiohttp.web.Request request: HTTP request object, that the
@@ -568,6 +572,9 @@ StreamResponse
568572
Send *HTTP header*. You should not change any header data after
569573
calling this method.
570574

575+
The coroutine calls :attr:`web.Application.on_response_prepare`
576+
signal handlers.
577+
571578
.. versionadded:: 0.18
572579

573580
.. method:: write(data)
@@ -920,6 +927,13 @@ arbitrary properties for later access from
920927

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

930+
.. attribute:: on_response_prepare
931+
932+
A :class:`~aiohttp.signals.Signal` that is fired at the beginning
933+
of :meth:`StreamResponse.prepare` with parameters *request* and
934+
*response*. It can be used, for example, to add custom headers to each
935+
response before sending.
936+
923937
.. method:: make_handler(**kwargs)
924938

925939
Creates HTTP protocol factory for handling requests.

tests/test_signals.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import asyncio
2+
from unittest import mock
3+
from aiohttp.multidict import CIMultiDict
4+
from aiohttp.signals import Signal
5+
from aiohttp.web import Application
6+
from aiohttp.web import Request, Response
7+
from aiohttp.protocol import HttpVersion11
8+
from aiohttp.protocol import RawRequestMessage
9+
10+
import pytest
11+
12+
13+
@pytest.fixture
14+
def app(loop):
15+
return Application(loop=loop)
16+
17+
18+
@pytest.fixture
19+
def debug_app(loop):
20+
return Application(loop=loop, debug=True)
21+
22+
23+
def make_request(app, method, path, headers=CIMultiDict()):
24+
message = RawRequestMessage(method, path, HttpVersion11, headers,
25+
False, False)
26+
return request_from_message(message, app)
27+
28+
29+
def request_from_message(message, app):
30+
payload = mock.Mock()
31+
transport = mock.Mock()
32+
reader = mock.Mock()
33+
writer = mock.Mock()
34+
req = Request(app, message, payload,
35+
transport, reader, writer)
36+
return req
37+
38+
39+
def test_add_response_prepare_signal_handler(loop, app):
40+
callback = asyncio.coroutine(lambda request, response: None)
41+
app.on_response_prepare.append(callback)
42+
43+
44+
def test_add_signal_handler_not_a_callable(loop, app):
45+
callback = True
46+
app.on_response_prepare.append(callback)
47+
with pytest.raises(TypeError):
48+
app.on_response_prepare(None, None)
49+
50+
51+
def test_function_signal_dispatch(loop, app):
52+
signal = Signal(app)
53+
kwargs = {'foo': 1, 'bar': 2}
54+
55+
callback_mock = mock.Mock()
56+
57+
@asyncio.coroutine
58+
def callback(**kwargs):
59+
callback_mock(**kwargs)
60+
61+
signal.append(callback)
62+
63+
loop.run_until_complete(signal.send(**kwargs))
64+
callback_mock.assert_called_once_with(**kwargs)
65+
66+
67+
def test_function_signal_dispatch2(loop, app):
68+
signal = Signal(app)
69+
args = {'a', 'b'}
70+
kwargs = {'foo': 1, 'bar': 2}
71+
72+
callback_mock = mock.Mock()
73+
74+
@asyncio.coroutine
75+
def callback(*args, **kwargs):
76+
callback_mock(*args, **kwargs)
77+
78+
signal.append(callback)
79+
80+
loop.run_until_complete(signal.send(*args, **kwargs))
81+
callback_mock.assert_called_once_with(*args, **kwargs)
82+
83+
84+
def test_response_prepare(loop, app):
85+
callback = mock.Mock()
86+
87+
@asyncio.coroutine
88+
def cb(*args, **kwargs):
89+
callback(*args, **kwargs)
90+
91+
app.on_response_prepare.append(cb)
92+
93+
request = make_request(app, 'GET', '/')
94+
response = Response(body=b'')
95+
loop.run_until_complete(response.prepare(request))
96+
97+
callback.assert_called_once_with(request=request,
98+
response=response)
99+
100+
101+
def test_non_coroutine(loop, app):
102+
signal = Signal(app)
103+
kwargs = {'foo': 1, 'bar': 2}
104+
105+
callback = mock.Mock()
106+
107+
signal.append(callback)
108+
109+
loop.run_until_complete(signal.send(**kwargs))
110+
callback.assert_called_once_with(**kwargs)
111+
112+
113+
def test_copy_forbidden(app):
114+
signal = Signal(app)
115+
with pytest.raises(NotImplementedError):
116+
signal.copy()
117+
118+
119+
def test_sort_forbidden(app):
120+
l1 = lambda: None
121+
l2 = lambda: None
122+
l3 = lambda: None
123+
signal = Signal(app)
124+
signal.extend([l1, l2, l3])
125+
with pytest.raises(NotImplementedError):
126+
signal.sort()
127+
assert signal == [l1, l2, l3]
128+
129+
130+
def test_debug_signal(loop, debug_app):
131+
assert debug_app.debug, "Should be True"
132+
signal = Signal(debug_app)
133+
134+
callback = mock.Mock()
135+
pre = mock.Mock()
136+
post = mock.Mock()
137+
138+
signal.append(callback)
139+
debug_app.on_pre_signal.append(pre)
140+
debug_app.on_post_signal.append(post)
141+
142+
loop.run_until_complete(signal.send(1, a=2))
143+
callback.assert_called_once_with(1, a=2)
144+
pre.assert_called_once_with(1, 'aiohttp.signals:Signal', 1, a=2)
145+
post.assert_called_once_with(1, 'aiohttp.signals:Signal', 1, a=2)

tests/test_web_exceptions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from aiohttp.web import Request
77
from aiohttp.protocol import RawRequestMessage, HttpVersion11
88

9-
from aiohttp import web
9+
from aiohttp import signals, web
1010

1111

1212
class TestHTTPExceptions(unittest.TestCase):
@@ -32,6 +32,8 @@ def append(self, data):
3232

3333
def make_request(self, method='GET', path='/', headers=CIMultiDict()):
3434
self.app = mock.Mock()
35+
self.app._debug = False
36+
self.app.on_response_prepare = signals.Signal(self.app)
3537
message = RawRequestMessage(method, path, HttpVersion11, headers,
3638
False, False)
3739
req = Request(self.app, message, self.payload,

tests/test_web_request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import unittest
33
from unittest import mock
4+
from aiohttp.signals import Signal
45
from aiohttp.web import Request
56
from aiohttp.multidict import MultiDict, CIMultiDict
67
from aiohttp.protocol import HttpVersion
@@ -23,6 +24,8 @@ def make_request(self, method, path, headers=CIMultiDict(), *,
2324
if version < HttpVersion(1, 1):
2425
closing = True
2526
self.app = mock.Mock()
27+
self.app._debug = False
28+
self.app.on_response_prepare = Signal(self.app)
2629
message = RawRequestMessage(method, path, version, headers, closing,
2730
False)
2831
self.payload = mock.Mock()

0 commit comments

Comments
 (0)