Skip to content

Commit a38efde

Browse files
author
Ignacio Avas
committed
Add Middleware and improve transparent patching
1 parent 76f69aa commit a38efde

File tree

12 files changed

+230
-729
lines changed

12 files changed

+230
-729
lines changed

request_query_count/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
__version__ = '0.1.0'
2+
default_app_config = 'request_query_count.apps.RequestQueryCountConfig'

request_query_count/apps.py

Lines changed: 131 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,142 @@
11
# -*- coding: utf-8
2+
import json
3+
import os
4+
import os.path
5+
import threading
6+
27
from django.apps import AppConfig
38
from django.conf import settings
4-
from django.core.checks import Error, register
5-
from django.utils import inspect
6-
from django.utils.module_loading import import_string
9+
from django.test import TransactionTestCase
10+
from django.test.utils import get_runner
11+
12+
from request_query_count.query_count import (TestCaseQueryContainer,
13+
TestResultQueryContainer)
714

8-
from request_query_count.query_count import Middleware
15+
local = threading.local()
916

1017

1118
class RequestQueryCountConfig(AppConfig):
19+
LOCAL_TESTCASE_CONTAINER_NAME = 'querycount_test_case_container'
20+
LOCAL_RESULT_CONTAINER_NAME = 'querycount_result_container'
21+
1222
name = 'request_query_count'
1323
verbose_name = 'Request Query Count'
1424

15-
def ready(self):
16-
pass
17-
18-
19-
@register
20-
def check_middleware(_app_configs, **_kwargs):
21-
errors = []
22-
23-
setting = getattr(settings, 'MIDDLEWARE', None)
24-
setting_name = 'MIDDLEWARE'
25-
if setting is None:
26-
setting = settings.MIDDLEWARE_CLASSES
27-
setting_name = 'MIDDLEWARE_CLASSES'
28-
29-
if not any(is_middleware_class(Middleware, middleware)
30-
for middleware in enumerate(setting)):
31-
errors.append(
32-
Error(
33-
"debug_toolbar.middleware.DebugToolbarMiddleware is missing "
34-
"from %s." % setting_name,
35-
hint="Add debug_toolbar.middleware.DebugToolbarMiddleware to "
36-
"%s." % setting_name,
37-
)
25+
setting_name = 'REQUEST_QUERY_COUNT'
26+
27+
default_settings = {
28+
'ENABLE': True,
29+
'ENABLE_STACKTRACES': True,
30+
'DETAIL_PATH': 'reports/query_count_detail.json',
31+
'SUMMARY_PATH': 'reports/query_count.json'
32+
}
33+
34+
@classmethod
35+
def get_setting(cls, setting_name):
36+
return (getattr(settings, cls.setting_name, {})
37+
.get(setting_name, cls.default_settings[setting_name]))
38+
39+
@classmethod
40+
def stacktraces_enabled(cls):
41+
return cls.get_setting('ENABLE_STACKTRACES')
42+
43+
@classmethod
44+
def enabled(cls):
45+
return cls.get_setting('ENABLE')
46+
47+
@classmethod
48+
def get_testcase_container(cls):
49+
return getattr(local, cls.LOCAL_TESTCASE_CONTAINER_NAME)
50+
51+
@classmethod
52+
def add_middleware(cls):
53+
setting = getattr(settings, 'MIDDLEWARE', None)
54+
setting_name = 'MIDDLEWARE'
55+
if setting is None:
56+
setting = settings.MIDDLEWARE_CLASSES
57+
setting_name = 'MIDDLEWARE_CLASSES'
58+
59+
setattr(
60+
settings,
61+
setting_name,
62+
setting + ('request_query_count.middleware.Middleware',)
3863
)
39-
return errors
40-
41-
42-
def is_middleware_class(middleware_class, middleware_path):
43-
try:
44-
middleware_cls = import_string(middleware_path)
45-
except ImportError:
46-
return
47-
return (
48-
inspect.isclass(middleware_cls) and
49-
issubclass(middleware_cls, middleware_class)
50-
)
64+
65+
@classmethod
66+
def wrap_set_up(cls, set_up):
67+
def wrapped(self, *args, **kwargs):
68+
result = set_up(self, *args, **kwargs)
69+
setattr(local, cls.LOCAL_TESTCASE_CONTAINER_NAME,
70+
TestCaseQueryContainer())
71+
return result
72+
73+
return wrapped
74+
75+
@classmethod
76+
def wrap_tear_down(cls, tear_down):
77+
def wrapped(self, *args, **kwargs):
78+
if not hasattr(cls, 'test_result_container'):
79+
return tear_down(self, *args, *kwargs)
80+
81+
container = cls.get_testcase_container()
82+
83+
test_method = getattr(self, self._testMethodName)
84+
85+
exclusions = (
86+
getattr(self.__class__, "__querycount_exclude__", []) +
87+
getattr(test_method, "__querycount_exclude__", [])
88+
)
89+
90+
all_queries = cls.test_result_container
91+
current_queries = container.filter_by(exclusions)
92+
all_queries.add(self.id(), current_queries)
93+
94+
return tear_down(self, *args, *kwargs)
95+
96+
return wrapped
97+
98+
@classmethod
99+
def patch_test_case(cls):
100+
TransactionTestCase.setUp = cls.wrap_set_up(TransactionTestCase.setUp)
101+
TransactionTestCase.tearDown = cls.wrap_tear_down(
102+
TransactionTestCase.tearDown)
103+
104+
@classmethod
105+
def save_json(cls, setting_name, container, detail):
106+
summary_path = os.path.realpath(cls.get_setting(setting_name))
107+
os.makedirs(os.path.dirname(summary_path), exist_ok=True)
108+
109+
with open(summary_path, 'w') as json_file:
110+
json.dump(container.get_json(detail=detail), json_file,
111+
ensure_ascii=False, indent=4, sort_keys=True)
112+
113+
@classmethod
114+
def wrap_testrunner_run(cls, func):
115+
def wrapped(self, *args, **kwargs):
116+
cls.test_result_container = TestResultQueryContainer()
117+
118+
result = func(self, *args, **kwargs)
119+
120+
cls.save_json('SUMMARY_PATH', cls.test_result_container, False)
121+
cls.save_json('DETAIL_PATH', cls.test_result_container, True)
122+
123+
result.queries = cls.test_result_container
124+
del cls.test_result_container
125+
126+
return result
127+
128+
return wrapped
129+
130+
@classmethod
131+
def patch_runner(cls):
132+
# FIXME: this is incompatible with --parallel and --test-runner
133+
# command arguments
134+
django_test_runner = get_runner(settings)
135+
test_runner = django_test_runner.test_runner
136+
test_runner.run = cls.wrap_testrunner_run(test_runner.run)
137+
138+
def ready(self):
139+
if self.enabled():
140+
self.add_middleware()
141+
self.patch_test_case()
142+
self.patch_runner()

request_query_count/middleware.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import DEFAULT_DB_ALIAS, connections
2+
from django.test.utils import CaptureQueriesContext
3+
4+
from request_query_count.apps import RequestQueryCountConfig
5+
6+
7+
class Middleware(object):
8+
def __init__(self, get_response):
9+
self.get_response = get_response
10+
11+
def __call__(self, request):
12+
if not RequestQueryCountConfig.enabled():
13+
# It is not necessary to capture queries
14+
return self.get_response(request)
15+
16+
with CaptureQueriesContext(connections[DEFAULT_DB_ALIAS]) as context:
17+
response = self.get_response(request)
18+
19+
query_container = RequestQueryCountConfig.get_testcase_container()
20+
query_container.add(request, context.captured_queries)
21+
22+
return response

request_query_count/models.py

Whitespace-only changes.

0 commit comments

Comments
 (0)