Merge lp:~fgallina/django-statsd/sync into lp:~ubuntuone-pqm-team/django-statsd/stable
- sync
- Merge into stable
Proposed by
Fabián Ezequiel Gallina
Status: | Merged |
---|---|
Merged at revision: | 78 |
Proposed branch: | lp:~fgallina/django-statsd/sync |
Merge into: | lp:~ubuntuone-pqm-team/django-statsd/stable |
Diff against target: |
446 lines (+252/-50) 9 files modified
.travis.yml (+1/-1) django_statsd/patches/__init__.py (+1/-5) django_statsd/patches/db.py (+37/-21) django_statsd/patches/utils.py (+15/-2) django_statsd/test_settings.py (+13/-0) django_statsd/tests.py (+167/-19) docs/index.rst (+16/-1) requirements.txt (+1/-0) setup.py (+1/-1) |
To merge this branch: | bzr merge lp:~fgallina/django-statsd/sync |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu One PQM Team | Pending | ||
Review via email: mp+217975@code.launchpad.net |
Commit message
Bump to latest released version
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.travis.yml' |
2 | --- .travis.yml 2012-09-12 17:33:25 +0000 |
3 | +++ .travis.yml 2014-05-01 19:34:04 +0000 |
4 | @@ -3,6 +3,6 @@ |
5 | - "2.6" |
6 | - "2.7" |
7 | install: pip install -r requirements.txt -r optional.txt --use-mirrors |
8 | -script: nosetests |
9 | +script: DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests |
10 | notifications: |
11 | irc: "irc.mozilla.org#amo-bots" |
12 | |
13 | === modified file 'django_statsd/patches/__init__.py' |
14 | --- django_statsd/patches/__init__.py 2012-06-20 12:58:58 +0000 |
15 | +++ django_statsd/patches/__init__.py 2014-05-01 19:34:04 +0000 |
16 | @@ -1,11 +1,7 @@ |
17 | from django.conf import settings |
18 | from django.utils.importlib import import_module |
19 | |
20 | -# Workaround for tests. |
21 | -try: |
22 | - patches = getattr(settings, 'STATSD_PATCHES', []) |
23 | -except ImportError: |
24 | - patches = [] |
25 | +patches = getattr(settings, 'STATSD_PATCHES', []) |
26 | |
27 | for patch in patches: |
28 | import_module(patch).patch() |
29 | |
30 | === modified file 'django_statsd/patches/db.py' |
31 | --- django_statsd/patches/db.py 2014-04-06 21:54:47 +0000 |
32 | +++ django_statsd/patches/db.py 2014-05-01 19:34:04 +0000 |
33 | @@ -1,45 +1,61 @@ |
34 | import django |
35 | from django.db.backends import util |
36 | - |
37 | -from django_statsd.patches.utils import wrap |
38 | +from django_statsd.patches.utils import wrap, patch_method |
39 | +from django_statsd.clients import statsd |
40 | |
41 | |
42 | def key(db, attr): |
43 | return 'db.%s.%s.%s' % (db.client.executable_name, db.alias, attr) |
44 | |
45 | |
46 | -def __getattr__(self, attr): |
47 | +def pre_django_1_6_cursorwrapper_getattr(self, attr): |
48 | """ |
49 | The CursorWrapper is a pretty small wrapper around the cursor. |
50 | If you are NOT in debug mode, this is the wrapper that's used. |
51 | Sadly if it's in debug mode, we get a different wrapper. |
52 | """ |
53 | - if django.VERSION < (1, 6) and self.db.is_managed(): |
54 | - # In Django 1.6 you can't put a connection in managed mode |
55 | + if self.db.is_managed(): |
56 | self.db.set_dirty() |
57 | if attr in self.__dict__: |
58 | return self.__dict__[attr] |
59 | else: |
60 | - if attr in ['execute', 'executemany']: |
61 | + if attr in ['execute', 'executemany', 'callproc']: |
62 | return wrap(getattr(self.cursor, attr), key(self.db, attr)) |
63 | return getattr(self.cursor, attr) |
64 | |
65 | |
66 | -def wrap_class(base): |
67 | - class Wrapper(base): |
68 | - def execute(self, *args, **kw): |
69 | - return wrap(super(Wrapper, self).execute, |
70 | - key(self.db, 'execute'))(*args, **kw) |
71 | - |
72 | - def executemany(self, *args, **kw): |
73 | - return wrap(super(Wrapper, self).executemany, |
74 | - key(self.db, 'executemany'))(*args, **kw) |
75 | - |
76 | - return Wrapper |
77 | +def patched_execute(orig_execute, self, *args, **kwargs): |
78 | + with statsd.timer(key(self.db, 'execute')): |
79 | + return orig_execute(self, *args, **kwargs) |
80 | + |
81 | + |
82 | +def patched_executemany(orig_executemany, self, *args, **kwargs): |
83 | + with statsd.timer(key(self.db, 'executemany')): |
84 | + return orig_executemany(self, *args, **kwargs) |
85 | + |
86 | + |
87 | +def patched_callproc(orig_callproc, self, *args, **kwargs): |
88 | + with statsd.timer(key(self.db, 'callproc')): |
89 | + return orig_callproc(self, *args, **kwargs) |
90 | |
91 | |
92 | def patch(): |
93 | - # So that it will work when DEBUG = True. |
94 | - util.CursorDebugWrapper = wrap_class(util.CursorDebugWrapper) |
95 | - # So that it will work when DEBUG = False. |
96 | - util.CursorWrapper.__getattr__ = __getattr__ |
97 | + """ |
98 | + The CursorWrapper is a pretty small wrapper around the cursor. If |
99 | + you are NOT in debug mode, this is the wrapper that's used. Sadly |
100 | + if it's in debug mode, we get a different wrapper for version |
101 | + earlier than 1.6. |
102 | + """ |
103 | + |
104 | + if django.VERSION > (1, 6): |
105 | + # In 1.6+ util.CursorDebugWrapper just makes calls to CursorWrapper |
106 | + # As such, we only need to instrument CursorWrapper. |
107 | + # Instrumenting both will result in duplicated metrics |
108 | + patch_method(util.CursorWrapper, 'execute')(patched_execute) |
109 | + patch_method(util.CursorWrapper, 'executemany')(patched_executemany) |
110 | + patch_method(util.CursorWrapper, 'callproc')(patched_callproc) |
111 | + else: |
112 | + util.CursorWrapper.__getattr__ = pre_django_1_6_cursorwrapper_getattr |
113 | + patch_method(util.CursorDebugWrapper, 'execute')(patched_execute) |
114 | + patch_method( |
115 | + util.CursorDebugWrapper, 'executemany')(patched_executemany) |
116 | |
117 | === modified file 'django_statsd/patches/utils.py' |
118 | --- django_statsd/patches/utils.py 2012-06-20 12:58:58 +0000 |
119 | +++ django_statsd/patches/utils.py 2014-05-01 19:34:04 +0000 |
120 | @@ -1,6 +1,19 @@ |
121 | from django_statsd.clients import statsd |
122 | -from functools import partial |
123 | - |
124 | +from functools import partial, wraps |
125 | + |
126 | +def patch_method(target, name, external_decorator=None): |
127 | + |
128 | + def decorator(patch_function): |
129 | + original_function = getattr(target, name) |
130 | + |
131 | + @wraps(patch_function) |
132 | + def wrapper(*args, **kw): |
133 | + return patch_function(original_function, *args, **kw) |
134 | + |
135 | + setattr(target, name, wrapper) |
136 | + return wrapper |
137 | + |
138 | + return decorator |
139 | |
140 | def wrapped(method, key, *args, **kw): |
141 | with statsd.timer(key): |
142 | |
143 | === added file 'django_statsd/test_settings.py' |
144 | --- django_statsd/test_settings.py 1970-01-01 00:00:00 +0000 |
145 | +++ django_statsd/test_settings.py 2014-05-01 19:34:04 +0000 |
146 | @@ -0,0 +1,13 @@ |
147 | +DATABASES = { |
148 | + 'default': { |
149 | + 'ENGINE': 'django.db.backends.sqlite3', |
150 | + 'NAME': 'mydatabase' |
151 | + } |
152 | +} |
153 | + |
154 | +ROOT_URLCONF = '' |
155 | +STATSD_CLIENT = 'django_statsd.clients.null' |
156 | +STATSD_PREFIX = None |
157 | +METLOG = None |
158 | + |
159 | +SECRET_KEY = 'secret' |
160 | |
161 | === modified file 'django_statsd/tests.py' |
162 | --- django_statsd/tests.py 2013-12-19 15:53:26 +0000 |
163 | +++ django_statsd/tests.py 2014-05-01 19:34:04 +0000 |
164 | @@ -5,23 +5,9 @@ |
165 | from django.conf import settings |
166 | from nose.exc import SkipTest |
167 | from nose import tools as nose_tools |
168 | - |
169 | -minimal = { |
170 | - 'DATABASES': { |
171 | - 'default': { |
172 | - 'ENGINE': 'django.db.backends.sqlite3', |
173 | - 'NAME': 'mydatabase' |
174 | - } |
175 | - }, |
176 | - 'ROOT_URLCONF': '', |
177 | - 'STATSD_CLIENT': 'django_statsd.clients.null', |
178 | - 'STATSD_PREFIX': None, |
179 | - 'METLOG': None |
180 | -} |
181 | - |
182 | -if not settings.configured: |
183 | - settings.configure(**minimal) |
184 | - |
185 | +from unittest2 import skipUnless |
186 | + |
187 | +from django import VERSION |
188 | from django.core.urlresolvers import reverse |
189 | from django.http import HttpResponse, HttpResponseForbidden |
190 | from django.test import TestCase |
191 | @@ -31,7 +17,13 @@ |
192 | |
193 | import mock |
194 | from nose.tools import eq_ |
195 | -from django_statsd.clients import get_client |
196 | +from django_statsd.clients import get_client, statsd |
197 | +from django_statsd.patches import utils |
198 | +from django_statsd.patches.db import ( |
199 | + patched_callproc, |
200 | + patched_execute, |
201 | + patched_executemany, |
202 | +) |
203 | from django_statsd import middleware |
204 | |
205 | cfg = { |
206 | @@ -261,7 +253,7 @@ |
207 | STATSD_CLIENT='django_statsd.clients.moz_metlog'): |
208 | client = get_client() |
209 | client.incr('foo', 2) |
210 | - |
211 | + |
212 | def test_metlog_prefixes(self): |
213 | metlog = self._create_client() |
214 | |
215 | @@ -407,3 +399,159 @@ |
216 | def test_not_emit(self, incr): |
217 | self.log.error('blargh!') |
218 | assert not incr.called |
219 | + |
220 | + |
221 | +class TestPatchMethod(TestCase): |
222 | + |
223 | + def setUp(self): |
224 | + super(TestPatchMethod, self).setUp() |
225 | + |
226 | + class DummyClass(object): |
227 | + |
228 | + def sumargs(self, a, b, c=3, d=4): |
229 | + return a + b + c + d |
230 | + |
231 | + def badfn(self, a, b=2): |
232 | + raise ValueError |
233 | + |
234 | + self.cls = DummyClass |
235 | + |
236 | + def test_late_patching(self): |
237 | + """ |
238 | + Objects created before patching should get patched as well. |
239 | + """ |
240 | + def patch_fn(original_fn, self, *args, **kwargs): |
241 | + return original_fn(self, *args, **kwargs) + 10 |
242 | + |
243 | + obj = self.cls() |
244 | + self.assertEqual(obj.sumargs(1, 2, 3, 4), 10) |
245 | + utils.patch_method(self.cls, 'sumargs')(patch_fn) |
246 | + self.assertEqual(obj.sumargs(1, 2, 3, 4), 20) |
247 | + |
248 | + def test_doesnt_call_original_implicitly(self): |
249 | + """ |
250 | + Original fn must be called explicitly from patched to be |
251 | + executed. |
252 | + """ |
253 | + def patch_fn(original_fn, self, *args, **kwargs): |
254 | + return 10 |
255 | + |
256 | + with self.assertRaises(ValueError): |
257 | + obj = self.cls() |
258 | + obj.badfn(1, 2) |
259 | + |
260 | + utils.patch_method(self.cls, 'badfn')(patch_fn) |
261 | + self.assertEqual(obj.badfn(1, 2), 10) |
262 | + |
263 | + def test_args_kwargs_are_honored(self): |
264 | + """ |
265 | + Args and kwargs must be honored between calls from the patched to |
266 | + the original version. |
267 | + """ |
268 | + def patch_fn(original_fn, self, *args, **kwargs): |
269 | + return original_fn(self, *args, **kwargs) |
270 | + |
271 | + utils.patch_method(self.cls, 'sumargs')(patch_fn) |
272 | + obj = self.cls() |
273 | + self.assertEqual(obj.sumargs(1, 2), 10) |
274 | + self.assertEqual(obj.sumargs(1, 1, d=1), 6) |
275 | + self.assertEqual(obj.sumargs(1, 1, 1, 1), 4) |
276 | + |
277 | + def test_patched_fn_can_receive_arbitrary_arguments(self): |
278 | + """ |
279 | + Args and kwargs can be received arbitrarily with no contraints on |
280 | + the patched fn, even if the original_fn had a fixed set of |
281 | + allowed args and kwargs. |
282 | + """ |
283 | + def patch_fn(original_fn, self, *args, **kwargs): |
284 | + return args, kwargs |
285 | + |
286 | + utils.patch_method(self.cls, 'badfn')(patch_fn) |
287 | + obj = self.cls() |
288 | + self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2})) |
289 | + self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2})) |
290 | + self.assertEqual(obj.badfn(1, 2, c=1, d=2), ((1, 2), {'c': 1, 'd': 2})) |
291 | + |
292 | + |
293 | +class TestCursorWrapperPatching(TestCase): |
294 | + |
295 | + def test_patched_callproc_calls_timer(self): |
296 | + with mock.patch.object(statsd, 'timer') as timer: |
297 | + db = mock.Mock(executable_name='name', alias='alias') |
298 | + instance = mock.Mock(db=db) |
299 | + patched_callproc(lambda *args, **kwargs: None, instance) |
300 | + self.assertEqual(timer.call_count, 1) |
301 | + |
302 | + def test_patched_execute_calls_timer(self): |
303 | + with mock.patch.object(statsd, 'timer') as timer: |
304 | + db = mock.Mock(executable_name='name', alias='alias') |
305 | + instance = mock.Mock(db=db) |
306 | + patched_execute(lambda *args, **kwargs: None, instance) |
307 | + self.assertEqual(timer.call_count, 1) |
308 | + |
309 | + def test_patched_executemany_calls_timer(self): |
310 | + with mock.patch.object(statsd, 'timer') as timer: |
311 | + db = mock.Mock(executable_name='name', alias='alias') |
312 | + instance = mock.Mock(db=db) |
313 | + patched_executemany(lambda *args, **kwargs: None, instance) |
314 | + self.assertEqual(timer.call_count, 1) |
315 | + |
316 | + @mock.patch( |
317 | + 'django_statsd.patches.db.pre_django_1_6_cursorwrapper_getattr') |
318 | + @mock.patch('django_statsd.patches.db.patched_executemany') |
319 | + @mock.patch('django_statsd.patches.db.patched_execute') |
320 | + @mock.patch('django.db.backends.util.CursorDebugWrapper') |
321 | + @skipUnless(VERSION < (1, 6, 0), "CursorWrapper Patching for Django<1.6") |
322 | + def test_cursorwrapper_patching(self, |
323 | + CursorDebugWrapper, |
324 | + execute, |
325 | + executemany, |
326 | + _getattr): |
327 | + try: |
328 | + from django.db.backends import util |
329 | + |
330 | + # We need to patch CursorWrapper like this because setting |
331 | + # __getattr__ on Mock instances raises AttributeError. |
332 | + class CursorWrapper(object): |
333 | + pass |
334 | + |
335 | + _CursorWrapper = util.CursorWrapper |
336 | + util.CursorWrapper = CursorWrapper |
337 | + |
338 | + from django_statsd.patches.db import patch |
339 | + execute.__name__ = 'execute' |
340 | + executemany.__name__ = 'executemany' |
341 | + _getattr.__name__ = '_getattr' |
342 | + execute.return_value = 'execute' |
343 | + executemany.return_value = 'executemany' |
344 | + _getattr.return_value = 'getattr' |
345 | + patch() |
346 | + |
347 | + self.assertEqual(CursorDebugWrapper.execute(), 'execute') |
348 | + self.assertEqual(CursorDebugWrapper.executemany(), 'executemany') |
349 | + self.assertEqual(CursorWrapper.__getattr__(), 'getattr') |
350 | + finally: |
351 | + util.CursorWrapper = _CursorWrapper |
352 | + |
353 | + @mock.patch('django_statsd.patches.db.patched_callproc') |
354 | + @mock.patch('django_statsd.patches.db.patched_executemany') |
355 | + @mock.patch('django_statsd.patches.db.patched_execute') |
356 | + @mock.patch('django.db.backends.util.CursorWrapper') |
357 | + @skipUnless(VERSION >= (1, 6, 0), "CursorWrapper Patching for Django>=1.6") |
358 | + def test_cursorwrapper_patching16(self, |
359 | + CursorWrapper, |
360 | + execute, |
361 | + executemany, |
362 | + callproc): |
363 | + from django_statsd.patches.db import patch |
364 | + execute.__name__ = 'execute' |
365 | + executemany.__name__ = 'executemany' |
366 | + callproc.__name__ = 'callproc' |
367 | + execute.return_value = 'execute' |
368 | + executemany.return_value = 'executemany' |
369 | + callproc.return_value = 'callproc' |
370 | + patch() |
371 | + |
372 | + self.assertEqual(CursorWrapper.execute(), 'execute') |
373 | + self.assertEqual(CursorWrapper.executemany(), 'executemany') |
374 | + self.assertEqual(CursorWrapper.callproc(), 'callproc') |
375 | |
376 | === modified file 'docs/index.rst' |
377 | --- docs/index.rst 2014-04-07 18:40:13 +0000 |
378 | +++ docs/index.rst 2014-05-01 19:34:04 +0000 |
379 | @@ -16,6 +16,10 @@ |
380 | Changes |
381 | ------- |
382 | |
383 | +0.3.12: |
384 | + |
385 | +- Event better Django 1.6 support for the patches, with tests. |
386 | + |
387 | 0.3.11: |
388 | |
389 | - Django 1.6 support |
390 | @@ -133,7 +137,7 @@ |
391 | statsd.incr('response.200') |
392 | |
393 | Django statsd will choose the client as specified in your config and send the |
394 | -data to it. You can change you client by specifying it in the config, the |
395 | +data to it. You can change your client by specifying it in the config, the |
396 | default is:: |
397 | |
398 | STATSD_CLIENT = 'django_statsd.clients.normal' |
399 | @@ -308,6 +312,13 @@ |
400 | }, |
401 | } |
402 | |
403 | +Testing |
404 | +======= |
405 | + |
406 | +You can run tests with the following command: |
407 | + |
408 | + DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests |
409 | + |
410 | Nose |
411 | ==== |
412 | |
413 | @@ -331,6 +342,10 @@ |
414 | * tomchristie |
415 | * diox |
416 | * frewsxcv |
417 | +* fud |
418 | +* ftobia |
419 | +* jawnb |
420 | +* fgallina |
421 | |
422 | See: |
423 | |
424 | |
425 | === modified file 'requirements.txt' |
426 | --- requirements.txt 2013-03-28 14:50:25 +0000 |
427 | +++ requirements.txt 2014-05-01 19:34:04 +0000 |
428 | @@ -1,4 +1,5 @@ |
429 | mock |
430 | nose |
431 | +unittest2 |
432 | statsd==1.0.0 |
433 | django<1.5 |
434 | |
435 | === modified file 'setup.py' |
436 | --- setup.py 2014-04-07 18:40:13 +0000 |
437 | +++ setup.py 2014-05-01 19:34:04 +0000 |
438 | @@ -4,7 +4,7 @@ |
439 | setup( |
440 | # Because django-statsd was taken, I called this django-statsd-mozilla. |
441 | name='django-statsd-mozilla', |
442 | - version='0.3.11', |
443 | + version='0.3.12', |
444 | description='Django interface with statsd', |
445 | long_description=open('README.rst').read(), |
446 | author='Andy McKay', |