Merge lp:~fgallina/django-statsd/sync into lp:~ubuntuone-pqm-team/django-statsd/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
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

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
=== modified file '.travis.yml'
--- .travis.yml 2012-09-12 17:33:25 +0000
+++ .travis.yml 2014-05-01 19:34:04 +0000
@@ -3,6 +3,6 @@
3 - "2.6"3 - "2.6"
4 - "2.7"4 - "2.7"
5install: pip install -r requirements.txt -r optional.txt --use-mirrors5install: pip install -r requirements.txt -r optional.txt --use-mirrors
6script: nosetests6script: DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests
7notifications:7notifications:
8 irc: "irc.mozilla.org#amo-bots"8 irc: "irc.mozilla.org#amo-bots"
99
=== modified file 'django_statsd/patches/__init__.py'
--- django_statsd/patches/__init__.py 2012-06-20 12:58:58 +0000
+++ django_statsd/patches/__init__.py 2014-05-01 19:34:04 +0000
@@ -1,11 +1,7 @@
1from django.conf import settings1from django.conf import settings
2from django.utils.importlib import import_module2from django.utils.importlib import import_module
33
4# Workaround for tests.4patches = getattr(settings, 'STATSD_PATCHES', [])
5try:
6 patches = getattr(settings, 'STATSD_PATCHES', [])
7except ImportError:
8 patches = []
95
10for patch in patches:6for patch in patches:
11 import_module(patch).patch()7 import_module(patch).patch()
128
=== modified file 'django_statsd/patches/db.py'
--- django_statsd/patches/db.py 2014-04-06 21:54:47 +0000
+++ django_statsd/patches/db.py 2014-05-01 19:34:04 +0000
@@ -1,45 +1,61 @@
1import django1import django
2from django.db.backends import util2from django.db.backends import util
33from django_statsd.patches.utils import wrap, patch_method
4from django_statsd.patches.utils import wrap4from django_statsd.clients import statsd
55
66
7def key(db, attr):7def key(db, attr):
8 return 'db.%s.%s.%s' % (db.client.executable_name, db.alias, attr)8 return 'db.%s.%s.%s' % (db.client.executable_name, db.alias, attr)
99
1010
11def __getattr__(self, attr):11def pre_django_1_6_cursorwrapper_getattr(self, attr):
12 """12 """
13 The CursorWrapper is a pretty small wrapper around the cursor.13 The CursorWrapper is a pretty small wrapper around the cursor.
14 If you are NOT in debug mode, this is the wrapper that's used.14 If you are NOT in debug mode, this is the wrapper that's used.
15 Sadly if it's in debug mode, we get a different wrapper.15 Sadly if it's in debug mode, we get a different wrapper.
16 """16 """
17 if django.VERSION < (1, 6) and self.db.is_managed():17 if self.db.is_managed():
18 # In Django 1.6 you can't put a connection in managed mode
19 self.db.set_dirty()18 self.db.set_dirty()
20 if attr in self.__dict__:19 if attr in self.__dict__:
21 return self.__dict__[attr]20 return self.__dict__[attr]
22 else:21 else:
23 if attr in ['execute', 'executemany']:22 if attr in ['execute', 'executemany', 'callproc']:
24 return wrap(getattr(self.cursor, attr), key(self.db, attr))23 return wrap(getattr(self.cursor, attr), key(self.db, attr))
25 return getattr(self.cursor, attr)24 return getattr(self.cursor, attr)
2625
2726
28def wrap_class(base):27def patched_execute(orig_execute, self, *args, **kwargs):
29 class Wrapper(base):28 with statsd.timer(key(self.db, 'execute')):
30 def execute(self, *args, **kw):29 return orig_execute(self, *args, **kwargs)
31 return wrap(super(Wrapper, self).execute,30
32 key(self.db, 'execute'))(*args, **kw)31
3332def patched_executemany(orig_executemany, self, *args, **kwargs):
34 def executemany(self, *args, **kw):33 with statsd.timer(key(self.db, 'executemany')):
35 return wrap(super(Wrapper, self).executemany,34 return orig_executemany(self, *args, **kwargs)
36 key(self.db, 'executemany'))(*args, **kw)35
3736
38 return Wrapper37def patched_callproc(orig_callproc, self, *args, **kwargs):
38 with statsd.timer(key(self.db, 'callproc')):
39 return orig_callproc(self, *args, **kwargs)
3940
4041
41def patch():42def patch():
42 # So that it will work when DEBUG = True.43 """
43 util.CursorDebugWrapper = wrap_class(util.CursorDebugWrapper)44 The CursorWrapper is a pretty small wrapper around the cursor. If
44 # So that it will work when DEBUG = False.45 you are NOT in debug mode, this is the wrapper that's used. Sadly
45 util.CursorWrapper.__getattr__ = __getattr__46 if it's in debug mode, we get a different wrapper for version
47 earlier than 1.6.
48 """
49
50 if django.VERSION > (1, 6):
51 # In 1.6+ util.CursorDebugWrapper just makes calls to CursorWrapper
52 # As such, we only need to instrument CursorWrapper.
53 # Instrumenting both will result in duplicated metrics
54 patch_method(util.CursorWrapper, 'execute')(patched_execute)
55 patch_method(util.CursorWrapper, 'executemany')(patched_executemany)
56 patch_method(util.CursorWrapper, 'callproc')(patched_callproc)
57 else:
58 util.CursorWrapper.__getattr__ = pre_django_1_6_cursorwrapper_getattr
59 patch_method(util.CursorDebugWrapper, 'execute')(patched_execute)
60 patch_method(
61 util.CursorDebugWrapper, 'executemany')(patched_executemany)
4662
=== modified file 'django_statsd/patches/utils.py'
--- django_statsd/patches/utils.py 2012-06-20 12:58:58 +0000
+++ django_statsd/patches/utils.py 2014-05-01 19:34:04 +0000
@@ -1,6 +1,19 @@
1from django_statsd.clients import statsd1from django_statsd.clients import statsd
2from functools import partial2from functools import partial, wraps
33
4def patch_method(target, name, external_decorator=None):
5
6 def decorator(patch_function):
7 original_function = getattr(target, name)
8
9 @wraps(patch_function)
10 def wrapper(*args, **kw):
11 return patch_function(original_function, *args, **kw)
12
13 setattr(target, name, wrapper)
14 return wrapper
15
16 return decorator
417
5def wrapped(method, key, *args, **kw):18def wrapped(method, key, *args, **kw):
6 with statsd.timer(key):19 with statsd.timer(key):
720
=== added file 'django_statsd/test_settings.py'
--- django_statsd/test_settings.py 1970-01-01 00:00:00 +0000
+++ django_statsd/test_settings.py 2014-05-01 19:34:04 +0000
@@ -0,0 +1,13 @@
1DATABASES = {
2 'default': {
3 'ENGINE': 'django.db.backends.sqlite3',
4 'NAME': 'mydatabase'
5 }
6}
7
8ROOT_URLCONF = ''
9STATSD_CLIENT = 'django_statsd.clients.null'
10STATSD_PREFIX = None
11METLOG = None
12
13SECRET_KEY = 'secret'
014
=== modified file 'django_statsd/tests.py'
--- django_statsd/tests.py 2013-12-19 15:53:26 +0000
+++ django_statsd/tests.py 2014-05-01 19:34:04 +0000
@@ -5,23 +5,9 @@
5from django.conf import settings5from django.conf import settings
6from nose.exc import SkipTest6from nose.exc import SkipTest
7from nose import tools as nose_tools7from nose import tools as nose_tools
88from unittest2 import skipUnless
9minimal = {9
10 'DATABASES': {10from django import VERSION
11 'default': {
12 'ENGINE': 'django.db.backends.sqlite3',
13 'NAME': 'mydatabase'
14 }
15 },
16 'ROOT_URLCONF': '',
17 'STATSD_CLIENT': 'django_statsd.clients.null',
18 'STATSD_PREFIX': None,
19 'METLOG': None
20}
21
22if not settings.configured:
23 settings.configure(**minimal)
24
25from django.core.urlresolvers import reverse11from django.core.urlresolvers import reverse
26from django.http import HttpResponse, HttpResponseForbidden12from django.http import HttpResponse, HttpResponseForbidden
27from django.test import TestCase13from django.test import TestCase
@@ -31,7 +17,13 @@
3117
32import mock18import mock
33from nose.tools import eq_19from nose.tools import eq_
34from django_statsd.clients import get_client20from django_statsd.clients import get_client, statsd
21from django_statsd.patches import utils
22from django_statsd.patches.db import (
23 patched_callproc,
24 patched_execute,
25 patched_executemany,
26)
35from django_statsd import middleware27from django_statsd import middleware
3628
37cfg = {29cfg = {
@@ -261,7 +253,7 @@
261 STATSD_CLIENT='django_statsd.clients.moz_metlog'):253 STATSD_CLIENT='django_statsd.clients.moz_metlog'):
262 client = get_client()254 client = get_client()
263 client.incr('foo', 2)255 client.incr('foo', 2)
264 256
265 def test_metlog_prefixes(self):257 def test_metlog_prefixes(self):
266 metlog = self._create_client()258 metlog = self._create_client()
267259
@@ -407,3 +399,159 @@
407 def test_not_emit(self, incr):399 def test_not_emit(self, incr):
408 self.log.error('blargh!')400 self.log.error('blargh!')
409 assert not incr.called401 assert not incr.called
402
403
404class TestPatchMethod(TestCase):
405
406 def setUp(self):
407 super(TestPatchMethod, self).setUp()
408
409 class DummyClass(object):
410
411 def sumargs(self, a, b, c=3, d=4):
412 return a + b + c + d
413
414 def badfn(self, a, b=2):
415 raise ValueError
416
417 self.cls = DummyClass
418
419 def test_late_patching(self):
420 """
421 Objects created before patching should get patched as well.
422 """
423 def patch_fn(original_fn, self, *args, **kwargs):
424 return original_fn(self, *args, **kwargs) + 10
425
426 obj = self.cls()
427 self.assertEqual(obj.sumargs(1, 2, 3, 4), 10)
428 utils.patch_method(self.cls, 'sumargs')(patch_fn)
429 self.assertEqual(obj.sumargs(1, 2, 3, 4), 20)
430
431 def test_doesnt_call_original_implicitly(self):
432 """
433 Original fn must be called explicitly from patched to be
434 executed.
435 """
436 def patch_fn(original_fn, self, *args, **kwargs):
437 return 10
438
439 with self.assertRaises(ValueError):
440 obj = self.cls()
441 obj.badfn(1, 2)
442
443 utils.patch_method(self.cls, 'badfn')(patch_fn)
444 self.assertEqual(obj.badfn(1, 2), 10)
445
446 def test_args_kwargs_are_honored(self):
447 """
448 Args and kwargs must be honored between calls from the patched to
449 the original version.
450 """
451 def patch_fn(original_fn, self, *args, **kwargs):
452 return original_fn(self, *args, **kwargs)
453
454 utils.patch_method(self.cls, 'sumargs')(patch_fn)
455 obj = self.cls()
456 self.assertEqual(obj.sumargs(1, 2), 10)
457 self.assertEqual(obj.sumargs(1, 1, d=1), 6)
458 self.assertEqual(obj.sumargs(1, 1, 1, 1), 4)
459
460 def test_patched_fn_can_receive_arbitrary_arguments(self):
461 """
462 Args and kwargs can be received arbitrarily with no contraints on
463 the patched fn, even if the original_fn had a fixed set of
464 allowed args and kwargs.
465 """
466 def patch_fn(original_fn, self, *args, **kwargs):
467 return args, kwargs
468
469 utils.patch_method(self.cls, 'badfn')(patch_fn)
470 obj = self.cls()
471 self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
472 self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
473 self.assertEqual(obj.badfn(1, 2, c=1, d=2), ((1, 2), {'c': 1, 'd': 2}))
474
475
476class TestCursorWrapperPatching(TestCase):
477
478 def test_patched_callproc_calls_timer(self):
479 with mock.patch.object(statsd, 'timer') as timer:
480 db = mock.Mock(executable_name='name', alias='alias')
481 instance = mock.Mock(db=db)
482 patched_callproc(lambda *args, **kwargs: None, instance)
483 self.assertEqual(timer.call_count, 1)
484
485 def test_patched_execute_calls_timer(self):
486 with mock.patch.object(statsd, 'timer') as timer:
487 db = mock.Mock(executable_name='name', alias='alias')
488 instance = mock.Mock(db=db)
489 patched_execute(lambda *args, **kwargs: None, instance)
490 self.assertEqual(timer.call_count, 1)
491
492 def test_patched_executemany_calls_timer(self):
493 with mock.patch.object(statsd, 'timer') as timer:
494 db = mock.Mock(executable_name='name', alias='alias')
495 instance = mock.Mock(db=db)
496 patched_executemany(lambda *args, **kwargs: None, instance)
497 self.assertEqual(timer.call_count, 1)
498
499 @mock.patch(
500 'django_statsd.patches.db.pre_django_1_6_cursorwrapper_getattr')
501 @mock.patch('django_statsd.patches.db.patched_executemany')
502 @mock.patch('django_statsd.patches.db.patched_execute')
503 @mock.patch('django.db.backends.util.CursorDebugWrapper')
504 @skipUnless(VERSION < (1, 6, 0), "CursorWrapper Patching for Django<1.6")
505 def test_cursorwrapper_patching(self,
506 CursorDebugWrapper,
507 execute,
508 executemany,
509 _getattr):
510 try:
511 from django.db.backends import util
512
513 # We need to patch CursorWrapper like this because setting
514 # __getattr__ on Mock instances raises AttributeError.
515 class CursorWrapper(object):
516 pass
517
518 _CursorWrapper = util.CursorWrapper
519 util.CursorWrapper = CursorWrapper
520
521 from django_statsd.patches.db import patch
522 execute.__name__ = 'execute'
523 executemany.__name__ = 'executemany'
524 _getattr.__name__ = '_getattr'
525 execute.return_value = 'execute'
526 executemany.return_value = 'executemany'
527 _getattr.return_value = 'getattr'
528 patch()
529
530 self.assertEqual(CursorDebugWrapper.execute(), 'execute')
531 self.assertEqual(CursorDebugWrapper.executemany(), 'executemany')
532 self.assertEqual(CursorWrapper.__getattr__(), 'getattr')
533 finally:
534 util.CursorWrapper = _CursorWrapper
535
536 @mock.patch('django_statsd.patches.db.patched_callproc')
537 @mock.patch('django_statsd.patches.db.patched_executemany')
538 @mock.patch('django_statsd.patches.db.patched_execute')
539 @mock.patch('django.db.backends.util.CursorWrapper')
540 @skipUnless(VERSION >= (1, 6, 0), "CursorWrapper Patching for Django>=1.6")
541 def test_cursorwrapper_patching16(self,
542 CursorWrapper,
543 execute,
544 executemany,
545 callproc):
546 from django_statsd.patches.db import patch
547 execute.__name__ = 'execute'
548 executemany.__name__ = 'executemany'
549 callproc.__name__ = 'callproc'
550 execute.return_value = 'execute'
551 executemany.return_value = 'executemany'
552 callproc.return_value = 'callproc'
553 patch()
554
555 self.assertEqual(CursorWrapper.execute(), 'execute')
556 self.assertEqual(CursorWrapper.executemany(), 'executemany')
557 self.assertEqual(CursorWrapper.callproc(), 'callproc')
410558
=== modified file 'docs/index.rst'
--- docs/index.rst 2014-04-07 18:40:13 +0000
+++ docs/index.rst 2014-05-01 19:34:04 +0000
@@ -16,6 +16,10 @@
16Changes16Changes
17-------17-------
1818
190.3.12:
20
21- Event better Django 1.6 support for the patches, with tests.
22
190.3.11:230.3.11:
2024
21- Django 1.6 support25- Django 1.6 support
@@ -133,7 +137,7 @@
133 statsd.incr('response.200')137 statsd.incr('response.200')
134138
135Django statsd will choose the client as specified in your config and send the139Django statsd will choose the client as specified in your config and send the
136data to it. You can change you client by specifying it in the config, the140data to it. You can change your client by specifying it in the config, the
137default is::141default is::
138142
139 STATSD_CLIENT = 'django_statsd.clients.normal'143 STATSD_CLIENT = 'django_statsd.clients.normal'
@@ -308,6 +312,13 @@
308 },312 },
309 }313 }
310314
315Testing
316=======
317
318You can run tests with the following command:
319
320 DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests
321
311Nose322Nose
312====323====
313324
@@ -331,6 +342,10 @@
331* tomchristie342* tomchristie
332* diox343* diox
333* frewsxcv344* frewsxcv
345* fud
346* ftobia
347* jawnb
348* fgallina
334349
335See:350See:
336351
337352
=== modified file 'requirements.txt'
--- requirements.txt 2013-03-28 14:50:25 +0000
+++ requirements.txt 2014-05-01 19:34:04 +0000
@@ -1,4 +1,5 @@
1mock1mock
2nose2nose
3unittest2
3statsd==1.0.04statsd==1.0.0
4django<1.55django<1.5
56
=== modified file 'setup.py'
--- setup.py 2014-04-07 18:40:13 +0000
+++ setup.py 2014-05-01 19:34:04 +0000
@@ -4,7 +4,7 @@
4setup(4setup(
5 # Because django-statsd was taken, I called this django-statsd-mozilla.5 # Because django-statsd was taken, I called this django-statsd-mozilla.
6 name='django-statsd-mozilla',6 name='django-statsd-mozilla',
7 version='0.3.11',7 version='0.3.12',
8 description='Django interface with statsd',8 description='Django interface with statsd',
9 long_description=open('README.rst').read(),9 long_description=open('README.rst').read(),
10 author='Andy McKay',10 author='Andy McKay',

Subscribers

People subscribed via source and target branches