Merge pull request #53 from fgallina/django-16-db-patches-fixes

Django 16 db patches fixes
This commit is contained in:
Andy McKay 2014-05-01 09:25:03 -07:00
commit bcda5ab911
7 changed files with 216 additions and 44 deletions

View File

@ -3,6 +3,6 @@ python:
- "2.6"
- "2.7"
install: pip install -r requirements.txt -r optional.txt --use-mirrors
script: nosetests
script: DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests
notifications:
irc: "irc.mozilla.org#amo-bots"

View File

@ -1,11 +1,7 @@
from django.conf import settings
from django.utils.importlib import import_module
# Workaround for tests.
try:
patches = getattr(settings, 'STATSD_PATCHES', [])
except ImportError:
patches = []
patches = getattr(settings, 'STATSD_PATCHES', [])
for patch in patches:
import_module(patch).patch()

View File

@ -7,6 +7,7 @@ from django_statsd.clients import statsd
def key(db, attr):
return 'db.%s.%s.%s' % (db.client.executable_name, db.alias, attr)
def pre_django_1_6_cursorwrapper_getattr(self, attr):
"""
The CursorWrapper is a pretty small wrapper around the cursor.
@ -22,33 +23,39 @@ def pre_django_1_6_cursorwrapper_getattr(self, attr):
return wrap(getattr(self.cursor, attr), key(self.db, attr))
return getattr(self.cursor, attr)
def patched_execute(orig_execute, self, *args, **kwargs):
with statsd.timer(key(self.db, 'execute')):
return orig_execute(self, *args, **kwargs)
def patched_executemany(orig_executemany, self, *args, **kwargs):
with statsd.timer(key(self.db, 'executemany')):
return orig_executemany(self, *args, **kwargs)
def patched_callproc(orig_callproc, self, *args, **kwargs):
with statsd.timer(key(self.db, 'callproc')):
return orig_callproc(self, *args, **kwargs)
def patch():
"""
The CursorWrapper is a pretty small wrapper around the cursor.
If you are NOT in debug mode, this is the wrapper that's used.
Sadly if it's in debug mode, we get a different wrapper for version earlier than 1.6.
The CursorWrapper is a pretty small wrapper around the cursor. If
you are NOT in debug mode, this is the wrapper that's used. Sadly
if it's in debug mode, we get a different wrapper for version
earlier than 1.6.
"""
def execute(orig_execute, self, *args, **kwargs):
with statsd.timer(key(self.db, 'execute')):
return orig_execute(self, *args, **kwargs)
def executemany(orig_executemany, self, *args, **kwargs):
with statsd.timer(key(self.db, 'executemany')):
return orig_executemany(self, *args, **kwargs)
def callproc(orig_callproc, self, *args, **kwargs):
with statsd.timer(key(self.db, 'callproc')):
return orig_callproc(self, *args, **kwargs)
if django.VERSION > (1, 6):
# In 1.6+ util.CursorDebugWrapper just makes calls to CursorWrapper
# As such, we only need to instrument CursorWrapper.
# Instrumenting both will result in duplicated metrics
patch_method(util.CursorWrapper, 'execute')(execute)
patch_method(util.CursorWrapper, 'executemany')(executemany)
patch_method(util.CursorWrapper, 'callproc')(callproc)
patch_method(util.CursorWrapper, 'execute')(patched_execute)
patch_method(util.CursorWrapper, 'executemany')(patched_executemany)
patch_method(util.CursorWrapper, 'callproc')(patched_callproc)
else:
util.CursorWrapper.__getattr__ = pre_django_1_6_cursorwrapper_getattr
patch_method(util.CursorDebugWrapper, 'execute')(execute)
patch_method(util.CursorDebugWrapper, 'executemany')(executemany)
patch_method(util.CursorDebugWrapper, 'execute')(patched_execute)
patch_method(
util.CursorDebugWrapper, 'executemany')(patched_executemany)

View File

@ -0,0 +1,13 @@
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'mydatabase'
}
}
ROOT_URLCONF = ''
STATSD_CLIENT = 'django_statsd.clients.null'
STATSD_PREFIX = None
METLOG = None
SECRET_KEY = 'secret'

View File

@ -5,23 +5,9 @@ import sys
from django.conf import settings
from nose.exc import SkipTest
from nose import tools as nose_tools
from unittest2 import skipUnless
minimal = {
'DATABASES': {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'mydatabase'
}
},
'ROOT_URLCONF': '',
'STATSD_CLIENT': 'django_statsd.clients.null',
'STATSD_PREFIX': None,
'METLOG': None
}
if not settings.configured:
settings.configure(**minimal)
from django import VERSION
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden
from django.test import TestCase
@ -31,7 +17,13 @@ from django.utils import unittest
import mock
from nose.tools import eq_
from django_statsd.clients import get_client
from django_statsd.clients import get_client, statsd
from django_statsd.patches import utils
from django_statsd.patches.db import (
patched_callproc,
patched_execute,
patched_executemany,
)
from django_statsd import middleware
cfg = {
@ -261,7 +253,7 @@ class TestMetlogClient(TestCase):
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
client.incr('foo', 2)
def test_metlog_prefixes(self):
metlog = self._create_client()
@ -407,3 +399,159 @@ class TestErrorLog(TestCase):
def test_not_emit(self, incr):
self.log.error('blargh!')
assert not incr.called
class TestPatchMethod(TestCase):
def setUp(self):
super(TestPatchMethod, self).setUp()
class DummyClass(object):
def sumargs(self, a, b, c=3, d=4):
return a + b + c + d
def badfn(self, a, b=2):
raise ValueError
self.cls = DummyClass
def test_late_patching(self):
"""
Objects created before patching should get patched as well.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs) + 10
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2, 3, 4), 10)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
self.assertEqual(obj.sumargs(1, 2, 3, 4), 20)
def test_doesnt_call_original_implicitly(self):
"""
Original fn must be called explicitly from patched to be
executed.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return 10
with self.assertRaises(ValueError):
obj = self.cls()
obj.badfn(1, 2)
utils.patch_method(self.cls, 'badfn')(patch_fn)
self.assertEqual(obj.badfn(1, 2), 10)
def test_args_kwargs_are_honored(self):
"""
Args and kwargs must be honored between calls from the patched to
the original version.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2), 10)
self.assertEqual(obj.sumargs(1, 1, d=1), 6)
self.assertEqual(obj.sumargs(1, 1, 1, 1), 4)
def test_patched_fn_can_receive_arbitrary_arguments(self):
"""
Args and kwargs can be received arbitrarily with no contraints on
the patched fn, even if the original_fn had a fixed set of
allowed args and kwargs.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return args, kwargs
utils.patch_method(self.cls, 'badfn')(patch_fn)
obj = self.cls()
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, 2, c=1, d=2), ((1, 2), {'c': 1, 'd': 2}))
class TestCursorWrapperPatching(TestCase):
def test_patched_callproc_calls_timer(self):
with mock.patch.object(statsd, 'timer') as timer:
db = mock.Mock(executable_name='name', alias='alias')
instance = mock.Mock(db=db)
patched_callproc(lambda *args, **kwargs: None, instance)
self.assertEqual(timer.call_count, 1)
def test_patched_execute_calls_timer(self):
with mock.patch.object(statsd, 'timer') as timer:
db = mock.Mock(executable_name='name', alias='alias')
instance = mock.Mock(db=db)
patched_execute(lambda *args, **kwargs: None, instance)
self.assertEqual(timer.call_count, 1)
def test_patched_executemany_calls_timer(self):
with mock.patch.object(statsd, 'timer') as timer:
db = mock.Mock(executable_name='name', alias='alias')
instance = mock.Mock(db=db)
patched_executemany(lambda *args, **kwargs: None, instance)
self.assertEqual(timer.call_count, 1)
@mock.patch(
'django_statsd.patches.db.pre_django_1_6_cursorwrapper_getattr')
@mock.patch('django_statsd.patches.db.patched_executemany')
@mock.patch('django_statsd.patches.db.patched_execute')
@mock.patch('django.db.backends.util.CursorDebugWrapper')
@skipUnless(VERSION < (1, 6, 0), "CursorWrapper Patching for Django<1.6")
def test_cursorwrapper_patching(self,
CursorDebugWrapper,
execute,
executemany,
_getattr):
try:
from django.db.backends import util
# We need to patch CursorWrapper like this because setting
# __getattr__ on Mock instances raises AttributeError.
class CursorWrapper(object):
pass
_CursorWrapper = util.CursorWrapper
util.CursorWrapper = CursorWrapper
from django_statsd.patches.db import patch
execute.__name__ = 'execute'
executemany.__name__ = 'executemany'
_getattr.__name__ = '_getattr'
execute.return_value = 'execute'
executemany.return_value = 'executemany'
_getattr.return_value = 'getattr'
patch()
self.assertEqual(CursorDebugWrapper.execute(), 'execute')
self.assertEqual(CursorDebugWrapper.executemany(), 'executemany')
self.assertEqual(CursorWrapper.__getattr__(), 'getattr')
finally:
util.CursorWrapper = _CursorWrapper
@mock.patch('django_statsd.patches.db.patched_callproc')
@mock.patch('django_statsd.patches.db.patched_executemany')
@mock.patch('django_statsd.patches.db.patched_execute')
@mock.patch('django.db.backends.util.CursorWrapper')
@skipUnless(VERSION >= (1, 6, 0), "CursorWrapper Patching for Django>=1.6")
def test_cursorwrapper_patching16(self,
CursorWrapper,
execute,
executemany,
callproc):
from django_statsd.patches.db import patch
execute.__name__ = 'execute'
executemany.__name__ = 'executemany'
callproc.__name__ = 'callproc'
execute.return_value = 'execute'
executemany.return_value = 'executemany'
callproc.return_value = 'callproc'
patch()
self.assertEqual(CursorWrapper.execute(), 'execute')
self.assertEqual(CursorWrapper.executemany(), 'executemany')
self.assertEqual(CursorWrapper.callproc(), 'callproc')

View File

@ -308,6 +308,13 @@ do this by adding in the handler. For example in your logging configuration::
},
}
Testing
=======
You can run tests with the following command:
DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests
Nose
====

View File

@ -1,4 +1,5 @@
mock
nose
unittest2
statsd==1.0.0
django<1.5