Merge lp:~fgallina/django-statsd/sync into lp:~ubuntuone-pqm-team/django-statsd/stable

Proposed by Fabián Ezequiel Gallina on 2014-05-01
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
Reviewer Review Type Date Requested Status
Ubuntu One PQM Team 2014-05-01 Pending
Review via email: mp+217975@code.launchpad.net

Commit message

Bump to latest released version

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',

Subscribers

People subscribed via source and target branches