Merge lp:~james-w/python-timeline-django/clean-env into lp:python-timeline-django

Proposed by James Westby
Status: Merged
Approved by: James Westby
Approved revision: 24
Merged at revision: 23
Proposed branch: lp:~james-w/python-timeline-django/clean-env
Merge into: lp:python-timeline-django
Diff against target: 164 lines (+79/-11)
4 files modified
timeline_django/hooks.py (+12/-7)
timeline_django/tests/test_hooks.py (+6/-0)
timeline_django/tests/test_wsgi.py (+48/-3)
timeline_django/wsgi.py (+13/-1)
To merge this branch: bzr merge lp:~james-w/python-timeline-django/clean-env
Reviewer Review Type Date Requested Status
Natalia Bidart (community) Approve
Review via email: mp+232704@code.launchpad.net

Commit message

Be more careful about leaking objects.

1. Register signals with a dispatch_uid so that if register is called
   twice there aren't double receivers. There is a very slim chance that
   someone could two want two sets of receivers with timeline_factories
   that get different timelines. In that case this should be rewritten
   to not use dispatch_uid and offer a disconnect method that can be
   used in tests etc.

2. Re-set the environ stored in the thread local after the wsgi response
   is delivered. In a case where some requests go through wsgi and some
   don't (e.g. tests) the environ would leak in to those that don't use
   wsgi. This could lead to an ever-growing timeline.

Description of the change

Hi,

This fixes the problems that timeline was causing in the sca test suite.

Thanks,

James

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Looks good!

review: Approve
24. By James Westby

Add missing space, thanks Natalia.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'timeline_django/hooks.py'
2--- timeline_django/hooks.py 2014-03-18 16:47:46 +0000
3+++ timeline_django/hooks.py 2014-08-29 14:46:17 +0000
4@@ -14,7 +14,6 @@
5 # GNU Lesser General Public License version 3 (see the file LICENSE).
6
7 from datetime import timedelta
8-import threading
9
10 import django.core.signals
11 import django.db.backends.signals
12@@ -87,9 +86,15 @@
13
14 def connect_to_signals(self):
15 """Connect the callbacks to their corresponding signals."""
16- django.core.signals.request_started.connect(self.request_started, weak=False)
17- django.core.signals.request_finished.connect(self.request_finished, weak=False)
18- django.core.signals.got_request_exception.connect(self.got_request_exception, weak=False)
19- django.db.backends.signals.connection_created.connect(self.connection_created, weak=False)
20- signals.wsgi_request_started.connect(self.wsgi_request_started, weak=False)
21- signals.wsgi_response_started.connect(self.wsgi_response_started, weak=False)
22+ django.core.signals.request_started.connect(
23+ self.request_started, weak=False, dispatch_uid="timeline_django")
24+ django.core.signals.request_finished.connect(
25+ self.request_finished, weak=False, dispatch_uid="timeline_django")
26+ django.core.signals.got_request_exception.connect(
27+ self.got_request_exception, weak=False, dispatch_uid="timeline_django")
28+ django.db.backends.signals.connection_created.connect(
29+ self.connection_created, weak=False, dispatch_uid="timeline_django")
30+ signals.wsgi_request_started.connect(
31+ self.wsgi_request_started, weak=False, dispatch_uid="timeline_django")
32+ signals.wsgi_response_started.connect(
33+ self.wsgi_response_started, weak=False, dispatch_uid="timeline_django")
34
35=== modified file 'timeline_django/tests/test_hooks.py'
36--- timeline_django/tests/test_hooks.py 2014-03-18 16:47:46 +0000
37+++ timeline_django/tests/test_hooks.py 2014-08-29 14:46:17 +0000
38@@ -252,28 +252,34 @@
39 signal, receiver = self.register_signal(django.core.signals, 'request_started')
40 self.assertEqual(1, len(signal.receivers))
41 self.assertEqual(receiver.request_started, signal.receivers[0][1])
42+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
43
44 def test_register_signals_registers_request_finished(self):
45 signal, receiver = self.register_signal(django.core.signals, 'request_finished')
46 self.assertEqual(1, len(signal.receivers))
47 self.assertEqual(receiver.request_finished, signal.receivers[0][1])
48+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
49
50 def test_register_signals_registers_got_request_exception(self):
51 signal, receiver = self.register_signal(django.core.signals, 'got_request_exception')
52 self.assertEqual(1, len(signal.receivers))
53 self.assertEqual(receiver.got_request_exception, signal.receivers[0][1])
54+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
55
56 def test_register_signals_registers_connection_created(self):
57 signal, receiver = self.register_signal(django.db.backends.signals, 'connection_created')
58 self.assertEqual(1, len(signal.receivers))
59 self.assertEqual(receiver.connection_created, signal.receivers[0][1])
60+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
61
62 def test_register_signals_registers_wsgi_request_started(self):
63 signal, receiver = self.register_signal(signals, 'wsgi_request_started')
64 self.assertEqual(1, len(signal.receivers))
65 self.assertEqual(receiver.wsgi_request_started, signal.receivers[0][1])
66+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
67
68 def test_register_signals_registers_wsgi_response_started(self):
69 signal, receiver = self.register_signal(signals, 'wsgi_response_started')
70 self.assertEqual(1, len(signal.receivers))
71 self.assertEqual(receiver.wsgi_response_started, signal.receivers[0][1])
72+ self.assertEqual("timeline_django", signal.receivers[0][0][0])
73
74=== modified file 'timeline_django/tests/test_wsgi.py'
75--- timeline_django/tests/test_wsgi.py 2013-09-03 00:25:21 +0000
76+++ timeline_django/tests/test_wsgi.py 2014-08-29 14:46:17 +0000
77@@ -37,12 +37,22 @@
78
79 def test_wsgi_sets_timeline(self):
80 environ = {}
81- start_response = lambda status, headers:None
82+ start_response = lambda status, headers: None
83+ def do_check(environ, start_response):
84+ self.assertThat(wsgi.get_environ(), Is(environ))
85+ yield "foo"
86+ app = wsgi.make_app(do_check)
87+ out = app(environ, start_response)
88+ self.assertEqual(["foo"], list(out))
89+
90+ def test_wsgi_unsets_timeline(self):
91+ environ = {}
92+ start_response = lambda status, headers: None
93 app = lambda environ, start_response: ["foo"]
94 app = wsgi.make_app(app)
95 out = app(environ, start_response)
96- self.assertEqual(["foo"], out)
97- self.assertThat(wsgi.get_environ(), Is(environ))
98+ self.assertEqual(["foo"], list(out))
99+ self.assertThat(wsgi.get_environ(), Is(None))
100
101 def patch_signal(self, name):
102 signal = Signal()
103@@ -93,3 +103,38 @@
104 wrapped_app(environ, None)
105 # Check that the saved environ matches the one passed to the app
106 self.assertEqual([environ], called_environs)
107+
108+
109+class GeneratorTrackerTests(TestCase):
110+
111+ def test_consumes_generator(self):
112+ app = wsgi.generator_tracker(lambda: True, ["foo"])
113+ self.assertEqual(["foo"], list(app))
114+
115+ def test_calls_on_finish(self):
116+ calls = []
117+ def generator():
118+ calls.append("consume")
119+ yield "foo"
120+ def on_finish():
121+ calls.append("on_finish")
122+ app = wsgi.generator_tracker(on_finish, generator())
123+ self.assertEqual(["foo"], list(app))
124+ self.assertEqual(["consume", "on_finish"], calls)
125+
126+ def test_calls_close(self):
127+ calls = []
128+ class Body(object):
129+
130+ def __iter__(self):
131+ calls.append("consume")
132+ yield "foo"
133+
134+ def close(self):
135+ calls.append("close")
136+
137+ def on_finish():
138+ calls.append("on_finish")
139+ app = wsgi.generator_tracker(on_finish, Body())
140+ self.assertEqual(["foo"], list(app))
141+ self.assertEqual(["consume", "close", "on_finish"], calls)
142
143=== modified file 'timeline_django/wsgi.py'
144--- timeline_django/wsgi.py 2013-09-03 00:25:21 +0000
145+++ timeline_django/wsgi.py 2014-08-29 14:46:17 +0000
146@@ -52,5 +52,17 @@
147 signals.wsgi_response_started.send(sender=save_environ,
148 environ=environ)
149 return start_response(*args, **kwargs)
150- return application(environ, signalled_start_response)
151+ def remove_env():
152+ _thread_local.environ = None
153+ return generator_tracker(remove_env, application(environ, signalled_start_response))
154 return save_environ
155+
156+
157+def generator_tracker(on_finish, app_body):
158+ try:
159+ for bytes in app_body:
160+ yield bytes
161+ finally:
162+ if hasattr(app_body, 'close'):
163+ app_body.close()
164+ on_finish()

Subscribers

People subscribed via source and target branches