Browse files

Merge pull request #1 from dcramer/master

latest
  • Loading branch information...
2 parents 8d8ed20 + 5d8abcf commit 7d3f5154f9f6b254bd70153367bd799417555ce5 @pombredanne committed Aug 14, 2012
View
9 CHANGES
@@ -0,0 +1,9 @@
+0.3.1
+
+* Fixed a bug when --wsgi-app was not provided.
+
+0.3
+
+* Added DEVSERVER_ARGS setting which is a list of CLI arguments to pass as defaults.
+* Added DEVSERVER_WSGI_MIDDLEWARE setting which is a list of additional WSGI middleware to apply.
+* Added --wsgi-app option to override the default WSGI application (does not inherit debug modules).
View
2 MANIFEST.in
@@ -0,0 +1,2 @@
+include setup.py README.rst LICENSE
+global-exclude *~
View
152 README.rst
@@ -6,11 +6,8 @@ A drop in replacement for Django's built-in runserver command. Features include:
* An extendable interface for handling things such as real-time logging.
* Integration with the werkzeug interactive debugger.
-* An improved runserver allowing you to process requests simultaneously.
-
-.. image:: http://www.pastethat.com/media/files/2010/02/10/Screen_shot_2010-02-10_at_10.05.31_PM.png
- :alt: devserver screenshot
-
+* Threaded (default) and multi-process development servers.
+* Ability to specify a WSGI application as your target environment.
------------
Installation
@@ -26,31 +23,15 @@ django-devserver has some optional dependancies, which we highly recommend insta
* ``pip install sqlparse`` -- pretty SQL formatting
* ``pip install werkzeug`` -- interactive debugger
* ``pip install guppy`` -- tracks memory usage (required for MemoryUseModule)
+* ``pip install line_profiler`` -- does line-by-line profiling (required for LineProfilerModule)
You will need to include ``devserver`` in your ``INSTALLED_APPS``::
INSTALLED_APPS = (
- 'devserver',
...
+ 'devserver',
)
-Specify modules to load via the ``DEVSERVER_MODULES`` setting::
-
- DEVSERVER_MODULES = (
- 'devserver.modules.sql.SQLRealTimeModule',
- 'devserver.modules.sql.SQLSummaryModule',
- 'devserver.modules.profile.ProfileSummaryModule',
-
- # Modules not enabled by default
- 'devserver.modules.ajax.AjaxDumpModule',
- 'devserver.modules.profile.MemoryUseModule',
- 'devserver.modules.cache.CacheSummaryModule',
- )
-
-You may also specify prefixes to skip processing for. By default, ``ADMIN_MEDIA_PREFIX`` and ``MEDIA_URL`` will be ignored (assuming ``MEDIA_URL`` is relative)::
-
- DEVSERVER_IGNORED_PREFIXES = ['/media', '/uploads']
-
-----
Usage
-----
@@ -61,49 +42,142 @@ Once installed, using the new runserver replacement is easy. You must specify ve
Note: This will force ``settings.DEBUG`` to ``True``.
-Please see ``python manage.py runserver --help`` for additional options.
+By default, ``devserver`` would bind itself to 127.0.0.1:8000. To change this default, ``DEVSERVER_DEFAULT_ADDR`` and ``DEVSERVER_DEFAULT_PORT`` settings are available.
+
+Additional CLI Options
+~~~~~~~~~~~~~~~~~~~~~~
+
+--werkzeug
+ Tells Django to use the Werkzeug interactive debugger, instead of it's own.
+
+--forked
+ Use a forking (multi-process) web server instead of threaded.
+
+--dozer
+ Enable the dozer memory debugging middleware (at /_dozer)
+
+--wsgi-app
+ Load the specified WSGI app as the server endpoint.
-You may also use devserver's middleware outside of the management command::
+Please see ``python manage.py runserver --help`` for more information additional options.
+
+Note: You may also use devserver's middleware outside of the management command::
MIDDLEWARE_CLASSES = (
- 'devserver.middleware.DevServerMiddleware',
+ 'devserver.middleware.DevServerMiddleware',
)
+-------------
+Configuration
+-------------
+
+The following options may be configured via your ``settings.py``:
+
+DEVSERVER_ARGS = []
+ Additional command line arguments to pass to the ``runserver`` command (as defaults).
+
+DEVSERVER_DEFAULT_ADDR = '127.0.0.1'
+ The default address to bind to.
+
+DEVSERVER_DEFAULT_PORT = '8000'
+ The default port to bind to.
+
+DEVSERVER_WSGI_MIDDLEWARE
+ A list of additional WSGI middleware to apply to the ``runserver`` command.
+
+DEVSERVER_MODULES = []
+ A list of devserver modules to load.
+
+DEVSERVER_IGNORED_PREFIXES = ['/media', '/uploads']
+ A list of prefixes to surpress and skip process on. By default, ``ADMIN_MEDIA_PREFIX``, ``MEDIA_URL`` and ``STATIC_URL`` (for Django >= 1.3) will be ignored (assuming ``MEDIA_URL`` and ``STATIC_URL`` is relative)::
+
+
-------
Modules
-------
-django-devserver includes several modules by default, but is also extendable by 3rd party modules.
+django-devserver includes several modules by default, but is also extendable by 3rd party modules. This is done via the ``DEVSERVER_MODULES`` setting::
+
+ DEVSERVER_MODULES = (
+ 'devserver.modules.sql.SQLRealTimeModule',
+ 'devserver.modules.sql.SQLSummaryModule',
+ 'devserver.modules.profile.ProfileSummaryModule',
+
+ # Modules not enabled by default
+ 'devserver.modules.ajax.AjaxDumpModule',
+ 'devserver.modules.profile.MemoryUseModule',
+ 'devserver.modules.cache.CacheSummaryModule',
+ 'devserver.modules.profile.LineProfilerModule',
+ )
devserver.modules.sql.SQLRealTimeModule
- Outputs queries as they happen to the terminal, including time taken.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Outputs queries as they happen to the terminal, including time taken.
- Disable SQL query truncation (used in SQLRealTimeModule) with the ``DEVSERVER_TRUNCATE_SQL`` setting::
+Disable SQL query truncation (used in SQLRealTimeModule) with the ``DEVSERVER_TRUNCATE_SQL`` setting::
- DEVSERVER_TRUNCATE_SQL = False
+ DEVSERVER_TRUNCATE_SQL = False
devserver.modules.sql.SQLSummaryModule
- Outputs a summary of your SQL usage.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Outputs a summary of your SQL usage.
devserver.modules.profile.ProfileSummaryModule
- Outputs a summary of the request performance.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Outputs a summary of the request performance.
devserver.modules.profile.MemoryUseModule
- Outputs a notice when memory use is increased (at the end of a request cycle).
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Outputs a notice when memory use is increased (at the end of a request cycle).
+
+devserver.modules.profile.LineProfilerModule
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Profiles view methods on a line by line basis. There are 2 ways to profile your view functions, by setting setting.DEVSERVER_AUTO_PROFILE = True or by decorating the view functions you want profiled with devserver.modules.profile.devserver_profile. The decoration takes an optional argument ``follow`` which is a sequence of functions that are called by your view function that you would also like profiled.
+
+An example of a decorated function::
+
+ @devserver_profile(follow=[foo, bar])
+ def home(request):
+ result['foo'] = foo()
+ result['bar'] = bar()
+
+When using the decorator, we recommend that rather than import the decoration directly from devserver that you have code somewhere in your project like::
+
+ try:
+ if 'devserver' not in settings.INSTALLED_APPS:
+ raise ImportError
+ from devserver.modules.profile import devserver_profile
+ except ImportError:
+ class devserver_profile(object):
+ def __init__(self, *args, **kwargs):
+ pass
+ def __call__(self, func):
+ def nothing(*args, **kwargs):
+ return func(*args, **kwargs)
+ return wraps(func)(nothing)
+
+By importing the decoration using this method, devserver_profile will be a pass through decoration if you aren't using devserver (eg in production)
+
devserver.modules.cache.CacheSummaryModule
- Outputs a summary of your cache calls at the end of the request.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Outputs a summary of your cache calls at the end of the request.
devserver.modules.ajax.AjaxDumpModule
- Outputs the content of any AJAX responses
-
- Change the maximum response length to dump with the ``DEVSERVER_AJAX_CONTENT_LENGTH`` setting::
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Outputs the content of any AJAX responses
- DEVSERVER_AJAX_CONTENT_LENGTH = 300
+Change the maximum response length to dump with the ``DEVSERVER_AJAX_CONTENT_LENGTH`` setting::
+
+ DEVSERVER_AJAX_CONTENT_LENGTH = 300
devserver.modules.request.SessionInfoModule
- Outputs information about the current session and user.
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Outputs information about the current session and user.
----------------
Building Modules
View
7 devserver/__init__.py
@@ -10,11 +10,12 @@
"""
__all__ = ('__version__', '__build__', '__docformat__', 'get_revision')
-__version__ = (0, 1, 6)
+__version__ = (0, 3, 2)
__docformat__ = 'restructuredtext en'
import os
+
def _get_git_revision(path):
revision_file = os.path.join(path, 'refs', 'heads', 'master')
if not os.path.exists(revision_file):
@@ -25,6 +26,7 @@ def _get_git_revision(path):
finally:
fh.close()
+
def get_revision():
"""
:returns: Revision number of this branch/checkout, if available. None if
@@ -39,8 +41,9 @@ def get_revision():
__build__ = get_revision()
+
def get_version():
base = '.'.join(map(str, __version__))
if __build__:
base = '%s (%s)' % (base, __build__)
- return base
+ return base
View
4 devserver/handlers.py
@@ -1,8 +1,8 @@
from django.core.handlers.wsgi import WSGIHandler
-from django.conf import settings
from devserver.middleware import DevServerMiddleware
+
class DevServerHandler(WSGIHandler):
def load_middleware(self):
super(DevServerHandler, self).load_middleware()
@@ -13,4 +13,4 @@ def load_middleware(self):
self._request_middleware.append(i.process_request)
self._view_middleware.append(i.process_view)
self._response_middleware.append(i.process_response)
- self._exception_middleware.append(i.process_exception)
+ self._exception_middleware.append(i.process_exception)
View
16 devserver/logger.py
@@ -7,27 +7,31 @@
from django.core.management.color import color_style
from django.utils import termcolors
+
_bash_colors = re.compile(r'\x1b\[[^m]*m')
+
+
def strip_bash_colors(string):
return _bash_colors.sub('', string)
+
class GenericLogger(object):
def __init__(self, module):
self.module = module
self.style = color_style()
-
+
def log(self, message, *args, **kwargs):
id = kwargs.pop('id', None)
duration = kwargs.pop('duration', None)
level = kwargs.pop('level', logging.INFO)
-
+
tpl_bits = []
if id:
tpl_bits.append(self.style.SQL_FIELD('[%s/%s]' % (self.module.logger_name, id)))
else:
tpl_bits.append(self.style.SQL_FIELD('[%s]' % self.module.logger_name))
if duration:
- tpl_bits.append(self.style.SQL_KEYWORD('(%.2fms)' % duration))
+ tpl_bits.append(self.style.SQL_KEYWORD('(%dms)' % duration))
if args:
message = message % args
@@ -50,9 +54,9 @@ def log(self, message, *args, **kwargs):
module=self.module.logger_name,
asctime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
)
-
+
indent = ' ' * (len(strip_bash_colors(tpl)) + 1)
-
+
new_message = []
first = True
for line in message.split('\n'):
@@ -71,4 +75,4 @@ def log(self, message, *args, **kwargs):
debug = lambda x, *a, **k: x.log(level=logging.DEBUG, *a, **k)
error = lambda x, *a, **k: x.log(level=logging.ERROR, *a, **k)
critical = lambda x, *a, **k: x.log(level=logging.CRITICAL, *a, **k)
- fatal = lambda x, *a, **k: x.log(level=logging.FATAL, *a, **k)
+ fatal = lambda x, *a, **k: x.log(level=logging.FATAL, *a, **k)
View
81 devserver/management/commands/runserver.py
@@ -1,20 +1,24 @@
-from django.core.management.base import BaseCommand, CommandError
+from django.conf import settings
+from django.core.management.base import BaseCommand, CommandError, handle_default_options
from django.core.servers.basehttp import AdminMediaHandler, WSGIServerException, \
WSGIServer
from django.core.handlers.wsgi import WSGIHandler
import os
import sys
import django
+import imp
import SocketServer
from optparse import make_option
from devserver.handlers import DevServerHandler
from devserver.utils.http import SlimWSGIRequestHandler
+
def null_technical_500_response(request, exc_type, exc_value, tb):
raise exc_type, exc_value, tb
+
def run(addr, port, wsgi_handler, mixin=None):
if mixin:
class new(mixin, WSGIServer):
@@ -27,6 +31,7 @@ def __init__(self, *args, **kwargs):
httpd.set_app(wsgi_handler)
httpd.serve_forever()
+
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--noreload', action='store_false', dest='use_reloader', default=True,
@@ -37,19 +42,41 @@ class Command(BaseCommand):
help='Specifies the directory from which to serve admin media.'),
make_option('--forked', action='store_true', dest='use_forked', default=False,
help='Use forking instead of threading for multiple web requests.'),
+ make_option('--dozer', action='store_true', dest='use_dozer', default=False,
+ help='Enable the Dozer memory debugging middleware.'),
+ make_option('--wsgi-app', dest='wsgi_app', default=None,
+ help='Load the specified WSGI app as the server endpoint.'),
)
+ if any(map(lambda app: app in settings.INSTALLED_APPS, ('django.contrib.staticfiles', 'staticfiles', ))):
+ option_list += make_option('--nostatic', dest='use_static_files', default=True,
+ help='Tells Django to NOT automatically serve static files at STATIC_URL.'),
+
help = "Starts a lightweight Web server for development which outputs additional debug information."
args = '[optional port number, or ipaddr:port]'
# Validation is called explicitly each time the server is reloaded.
requires_model_validation = False
+ def run_from_argv(self, argv):
+ parser = self.create_parser(argv[0], argv[1])
+ default_args = getattr(settings, 'DEVSERVER_ARGS', None)
+ if default_args:
+ options, args = parser.parse_args(default_args)
+ else:
+ options = None
+
+ options, args = parser.parse_args(argv[2:], options)
+
+ handle_default_options(options)
+ self.execute(*args, **options.__dict__)
+
def handle(self, addrport='', *args, **options):
if args:
raise CommandError('Usage is runserver %s' % self.args)
+
if not addrport:
- addr = ''
- port = '8000'
+ addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '')
+ port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', '8000')
else:
try:
addr, port = addrport.split(':')
@@ -66,18 +93,20 @@ def handle(self, addrport='', *args, **options):
shutdown_message = options.get('shutdown_message', '')
use_werkzeug = options.get('use_werkzeug', False)
quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C'
+ wsgi_app = options.get('wsgi_app', None)
+ use_static_files = options.get('use_static_files', True)
if use_werkzeug:
try:
from werkzeug import run_simple, DebuggedApplication
except ImportError, e:
+ print >> sys.stderr, "WARNING: Unable to initialize werkzeug: %s" % e
use_werkzeug = False
else:
use_werkzeug = True
from django.views import debug
debug.technical_500_response = null_technical_500_response
-
def inner_run():
# Flag the server as active
from devserver import settings
@@ -92,7 +121,11 @@ def inner_run():
self.validate(display_num_errors=True)
print "\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE)
print "Running django-devserver %s" % (devserver.get_version(),)
- print "%s server is running at http://%s:%s/" % (options['use_forked'] and 'Forked' or 'Threaded', addr, port)
+ if use_werkzeug:
+ server_type = 'werkzeug'
+ else:
+ server_type = 'django'
+ print "%s %s server is running at http://%s:%s/" % (options['use_forked'] and 'Forked' or 'Threaded', server_type, addr, port)
print "Quit the server with %s." % quit_command
# django.core.management.base forces the locale to en-us. We should
@@ -101,22 +134,47 @@ def inner_run():
translation.activate(settings.LANGUAGE_CODE)
if int(options['verbosity']) < 1:
- base_handler = WSGIHandler
+ app = WSGIHandler()
else:
- base_handler = DevServerHandler
+ app = DevServerHandler()
+
+ if wsgi_app:
+ print "Using WSGI application %r" % wsgi_app
+ if os.path.exists(os.path.abspath(wsgi_app)):
+ # load from file
+ app = imp.load_source('wsgi_app', os.path.abspath(wsgi_app)).application
+ else:
+ try:
+ app = __import__(wsgi_app, {}, {}, ['application']).application
+ except (ImportError, AttributeError):
+ raise
if options['use_forked']:
mixin = SocketServer.ForkingMixIn
else:
mixin = SocketServer.ThreadingMixIn
+ middleware = getattr(settings, 'DEVSERVER_WSGI_MIDDLEWARE', [])
+ for middleware in middleware:
+ module, class_name = middleware.rsplit('.', 1)
+ app = getattr(__import__(module, {}, {}, [class_name]), class_name)(app)
+
+ if 'django.contrib.staticfiles' in settings.INSTALLED_APPS and use_static_files:
+ from django.contrib.staticfiles.handlers import StaticFilesHandler
+ app = StaticFilesHandler(app)
+ else:
+ app = AdminMediaHandler(app, admin_media_path)
+
+ if options['use_dozer']:
+ from dozer import Dozer
+ app = Dozer(app)
+
try:
- handler = AdminMediaHandler(base_handler(), admin_media_path)
if use_werkzeug:
- run_simple(addr, int(port), DebuggedApplication(handler, True),
- use_reloader=use_reloader, use_debugger=True)
+ run_simple(addr, int(port), DebuggedApplication(app, True),
+ use_reloader=False, use_debugger=True)
else:
- run(addr, int(port), handler, mixin)
+ run(addr, int(port), app, mixin)
except WSGIServerException, e:
# Use helpful error messages instead of ugly tracebacks.
ERRORS = {
@@ -136,6 +194,7 @@ def inner_run():
print shutdown_message
sys.exit(0)
+ # werkzeug does its own autoreload stuff
if use_reloader:
from django.utils import autoreload
autoreload.main(inner_run)
View
47 devserver/middleware.py
@@ -1,45 +1,60 @@
from devserver.models import MODULES
+
class DevServerMiddleware(object):
def should_process(self, request):
from django.conf import settings
-
- if settings.MEDIA_URL and request.path.startswith(settings.MEDIA_URL):
+
+ if getattr(settings, 'STATIC_URL', None) and request.build_absolute_uri().startswith(request.build_absolute_uri(settings.STATIC_URL)):
return False
-
- if settings.ADMIN_MEDIA_PREFIX and request.path.startswith(settings.ADMIN_MEDIA_PREFIX):
+
+ if settings.MEDIA_URL and request.build_absolute_uri().startswith(request.build_absolute_uri(settings.MEDIA_URL)):
+ return False
+
+ if getattr(settings, 'ADMIN_MEDIA_PREFIX', None) and request.path.startswith(settings.ADMIN_MEDIA_PREFIX):
return False
-
+
if request.path == '/favicon.ico':
return False
for path in getattr(settings, 'DEVSERVER_IGNORED_PREFIXES', []):
if request.path.startswith(path):
return False
-
+
return True
-
+
def process_request(self, request):
+ # Set a sentinel value which process_response can use to abort when
+ # another middleware app short-circuits processing:
+ request._devserver_active = True
+
self.process_init(request)
-
+
if self.should_process(request):
for mod in MODULES:
mod.process_request(request)
-
+
def process_response(self, request, response):
+ # If this isn't set, it usually means that another middleware layer
+ # has returned an HttpResponse and the following middleware won't see
+ # the request. This happens most commonly with redirections - see
+ # https://github.com/dcramer/django-devserver/issues/28 for details:
+ if not getattr(request, "_devserver_active", False):
+ return response
+
if self.should_process(request):
for mod in MODULES:
mod.process_response(request, response)
-
+
self.process_complete(request)
-
+
return response
-
+
def process_exception(self, request, exception):
if self.should_process(request):
for mod in MODULES:
mod.process_exception(request, exception)
-
+
def process_view(self, request, view_func, view_args, view_kwargs):
if self.should_process(request):
for mod in MODULES:
@@ -48,14 +63,14 @@ def process_view(self, request, view_func, view_args, view_kwargs):
def process_init(self, request):
from devserver.utils.stats import stats
-
+
stats.reset()
-
+
if self.should_process(request):
for mod in MODULES:
mod.process_init(request)
def process_complete(self, request):
if self.should_process(request):
for mod in MODULES:
- mod.process_complete(request)
+ mod.process_complete(request)
View
15 devserver/models.py
@@ -2,15 +2,17 @@
from devserver.logger import GenericLogger
+
MODULES = []
+
+
def load_modules():
global MODULES
-
+
MODULES = []
-
-
+
from devserver import settings
-
+
for path in settings.DEVSERVER_MODULES:
try:
name, class_name = path.rsplit('.', 1)
@@ -30,8 +32,9 @@ def load_modules():
try:
instance = cls(GenericLogger(cls))
except:
- raise # Bubble up problem loading panel
+ raise # Bubble up problem loading panel
MODULES.append(instance)
+
if not MODULES:
- load_modules()
+ load_modules()
View
14 devserver/modules/__init__.py
@@ -3,24 +3,24 @@ class DevServerModule(object):
Functions a lot like middleware, except that it does not accept any return values.
"""
logger_name = 'generic'
-
+
def __init__(self, logger):
self.logger = logger
-
+
def process_request(self, request):
pass
-
+
def process_response(self, request, response):
pass
-
+
def process_exception(self, request, exception):
pass
def process_view(self, request, view_func, view_args, view_kwargs):
pass
-
+
def process_init(self, request):
pass
-
+
def process_complete(self, request):
- pass
+ pass
View
10 devserver/modules/ajax.py
@@ -1,15 +1,21 @@
+import json
+
from devserver.modules import DevServerModule
from devserver import settings
+
class AjaxDumpModule(DevServerModule):
"""
Dumps the content of all AJAX responses.
"""
logger_name = 'ajax'
-
+
def process_response(self, request, response):
if request.is_ajax():
# Let's do a quick test to see what kind of response we have
if len(response.content) < settings.DEVSERVER_AJAX_CONTENT_LENGTH:
- self.logger.info(response.content)
+ content = response.content
+ if settings.DEVSERVER_AJAX_PRETTY_PRINT:
+ content = json.dumps(json.loads(content), indent=4)
+ self.logger.info(content)
View
26 devserver/modules/cache.py
@@ -2,23 +2,25 @@
from devserver.modules import DevServerModule
+
class CacheSummaryModule(DevServerModule):
"""
Outputs a summary of cache events once a response is ready.
"""
+ real_time = False
logger_name = 'cache'
attrs_to_track = ['set', 'get', 'delete', 'add', 'get_many']
-
+
def process_init(self, request):
from devserver.utils.stats import track
# save our current attributes
self.old = dict((k, getattr(cache, k)) for k in self.attrs_to_track)
for k in self.attrs_to_track:
- setattr(cache, k, track(getattr(cache, k), 'cache'))
+ setattr(cache, k, track(getattr(cache, k), 'cache', self.logger if self.real_time else None))
def process_complete(self, request):
from devserver.utils.stats import stats
@@ -31,15 +33,19 @@ def process_complete(self, request):
ratio = int(hits / float(misses + hits) * 100)
else:
ratio = 100
-
- self.logger.info('%(calls)s calls made with a %(ratio)d%% hit percentage (%(misses)s misses)' % dict(
- calls = calls,
- ratio = ratio,
- hits = hits,
- misses = misses,
- ), duration=stats.get_total_time('cache'))
+
+ if not self.real_time:
+ self.logger.info('%(calls)s calls made with a %(ratio)d%% hit percentage (%(misses)s misses)' % dict(
+ calls=calls,
+ ratio=ratio,
+ hits=hits,
+ misses=misses,
+ ), duration=stats.get_total_time('cache'))
# set our attributes back to their defaults
for k, v in self.old.iteritems():
setattr(cache, k, v)
-
+
+
+class CacheRealTimeModule(CacheSummaryModule):
+ real_time = True
View
102 devserver/modules/profile.py
@@ -1,33 +1,37 @@
from devserver.modules import DevServerModule
from devserver.utils.time import ms_from_timedelta
+from devserver.settings import DEVSERVER_AUTO_PROFILE
from datetime import datetime
+import functools
import gc
+
class ProfileSummaryModule(DevServerModule):
"""
Outputs a summary of cache events once a response is ready.
"""
logger_name = 'profile'
-
+
def process_init(self, request):
self.start = datetime.now()
def process_complete(self, request):
duration = datetime.now() - self.start
-
+
self.logger.info('Total time to render was %.2fs', ms_from_timedelta(duration) / 1000)
+
class LeftOversModule(DevServerModule):
"""
Outputs a summary of events the garbage collector couldn't handle.
"""
# TODO: Not even sure this is correct, but the its a general idea
logger_name = 'profile'
-
+
def process_init(self, request):
gc.enable()
gc.set_debug(gc.DEBUG_SAVEALL)
@@ -53,21 +57,83 @@ class MemoryUseModule(DevServerModule):
Outputs a summary of memory usage of the course of a request.
"""
logger_name = 'profile'
-
- def process_init(self, request):
- from guppy import hpy
-
- self.usage = 0
- self.heapy = hpy()
- self.heapy.setrelheap()
+ def __init__(self, request):
+ super(MemoryUseModule, self).__init__(request)
+ self.hpy = hpy()
+ self.oldh = self.hpy.heap()
+ self.logger.info('heap size is %s', filesizeformat(self.oldh.size))
+
+ def process_complete(self, request):
+ newh = self.hpy.heap()
+ alloch = newh - self.oldh
+ dealloch = self.oldh - newh
+ self.oldh = newh
+ self.logger.info('%s allocated, %s deallocated, heap size is %s', *map(filesizeformat, [alloch.size, dealloch.size, newh.size]))
+
+try:
+ from line_profiler import LineProfiler
+except ImportError:
+ import warnings
+
+ class LineProfilerModule(DevServerModule):
+
+ def __new__(cls, *args, **kwargs):
+ warnings.warn('LineProfilerModule requires line_profiler to be installed.')
+ return super(LineProfilerModule, cls).__new__(cls)
+
+ class devserver_profile(object):
+ def __init__(self, follow=[]):
+ pass
+
+ def __call__(self, func):
+ return func
+else:
+ class LineProfilerModule(DevServerModule):
+ """
+ Outputs a Line by Line profile of any @devserver_profile'd functions that were run
+ """
+ logger_name = 'profile'
+
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ request.devserver_profiler = LineProfiler()
+ request.devserver_profiler_run = False
+ if (DEVSERVER_AUTO_PROFILE):
+ _unwrap_closure_and_profile(request.devserver_profiler, view_func)
+ request.devserver_profiler.enable_by_count()
def process_complete(self, request):
- h = self.heapy.heap()
-
- if h.domisize > self.usage:
- self.usage = h.domisize
-
- if self.usage:
- self.logger.info('Memory usage was increased by %s', filesizeformat(self.usage))
-
+ if hasattr(request, 'devserver_profiler_run') and (DEVSERVER_AUTO_PROFILE or request.devserver_profiler_run):
+ from cStringIO import StringIO
+ out = StringIO()
+ if (DEVSERVER_AUTO_PROFILE):
+ request.devserver_profiler.disable_by_count()
+ request.devserver_profiler.print_stats(stream=out)
+ self.logger.info(out.getvalue())
+
+ def _unwrap_closure_and_profile(profiler, func):
+ if not hasattr(func, 'func_code'):
+ return
+ profiler.add_function(func)
+ if func.func_closure:
+ for cell in func.func_closure:
+ if hasattr(cell.cell_contents, 'func_code'):
+ _unwrap_closure_and_profile(profiler, cell.cell_contents)
+
+ class devserver_profile(object):
+ def __init__(self, follow=[]):
+ self.follow = follow
+
+ def __call__(self, func):
+ def profiled_func(request, *args, **kwargs):
+ try:
+ request.devserver_profiler.add_function(func)
+ request.devserver_profiler_run = True
+ for f in self.follow:
+ request.devserver_profiler.add_function(f)
+ request.devserver_profiler.enable_by_count()
+ retval = func(request, *args, **kwargs)
+ finally:
+ request.devserver_profiler.disable_by_count()
+ return retval
+ return functools.wraps(func)(profiled_func)
View
47 devserver/modules/request.py
@@ -1,19 +1,22 @@
+import urllib
+
from devserver.modules import DevServerModule
+
class SessionInfoModule(DevServerModule):
"""
Displays information about the currently authenticated user and session.
"""
logger_name = 'session'
-
+
def process_request(self, request):
self.has_session = bool(getattr(request, 'session', False))
if self.has_session is not None:
self._save = request.session.save
self.session = request.session
request.session.save = self.handle_session_save
-
+
def process_response(self, request, response):
if getattr(self, 'has_session', False):
if getattr(request, 'user', None) and request.user.is_authenticated():
@@ -24,7 +27,43 @@ def process_response(self, request, response):
request.session.save = self._save
self._save = None
self.session = None
-
+ self.has_session = False
+
def handle_session_save(self, *args, **kwargs):
self._save(*args, **kwargs)
- self.logger.info('Session %s has been saved.', self.session.session_key)
+ self.logger.info('Session %s has been saved.', self.session.session_key)
+
+
+class RequestDumpModule(DevServerModule):
+ """
+ Dumps the request headers and variables.
+ """
+
+ logger_name = 'request'
+
+ def process_request(self, request):
+ req = self.logger.style.SQL_KEYWORD('%s %s %s\n' % (request.method, '?'.join((request.META['PATH_INFO'], request.META['QUERY_STRING'])), request.META['SERVER_PROTOCOL']))
+ for var, val in request.META.items():
+ if var.startswith('HTTP_'):
+ var = var[5:].replace('_', '-').title()
+ req += '%s: %s\n' % (self.logger.style.SQL_KEYWORD(var), val)
+ if request.META['CONTENT_LENGTH']:
+ req += '%s: %s\n' % (self.logger.style.SQL_KEYWORD('Content-Length'), request.META['CONTENT_LENGTH'])
+ if request.POST:
+ req += '\n%s\n' % self.logger.style.HTTP_INFO(urllib.urlencode(dict((k, v.encode('utf8')) for k, v in request.POST.items())))
+ if request.FILES:
+ req += '\n%s\n' % self.logger.style.HTTP_NOT_MODIFIED(urllib.urlencode(request.FILES))
+ self.logger.info('Full request:\n%s', req)
+
+class ResponseDumpModule(DevServerModule):
+ """
+ Dumps the request headers and variables.
+ """
+
+ logger_name = 'response'
+
+ def process_response(self, request, response):
+ res = self.logger.style.SQL_FIELD('Status code: %s\n' % response.status_code)
+ res += '\n'.join(['%s: %s' % (self.logger.style.SQL_FIELD(k), v)
+ for k, v in response._headers.values()])
+ self.logger.info('Full response:\n%s', res)
View
114 devserver/modules/sql.py
@@ -1,13 +1,17 @@
"""
Based on initial work from django-debug-toolbar
"""
+import re
from datetime import datetime
-import sys
-import traceback
-import django
-from django.db import connection
+try:
+ from django.db import connections
+except ImportError:
+ # Django version < 1.2
+ from django.db import connection
+ connections = {'default': connection}
+
from django.db.backends import util
#from django.template import Node
@@ -24,9 +28,11 @@ class sqlparse:
def format(text, *args, **kwargs):
return text
-import re
+
_sql_fields_re = re.compile(r'SELECT .*? FROM')
_sql_aggregates_re = re.compile(r'SELECT .*?(COUNT|SUM|AVERAGE|MIN|MAX).*? FROM')
+
+
def truncate_sql(sql, aggregates=True):
if not aggregates and _sql_aggregates_re.match(sql):
return sql
@@ -37,105 +43,105 @@ def truncate_sql(sql, aggregates=True):
# SQL_WARNING_THRESHOLD = getattr(settings, 'DEVSERVER_CONFIG', {}) \
# .get('SQL_WARNING_THRESHOLD', 500)
-class DatabaseStatTracker(util.CursorDebugWrapper):
+try:
+ from debug_toolbar.panels.sql import DatabaseStatTracker
+ debug_toolbar = True
+except ImportError:
+ debug_toolbar = False
+ DatabaseStatTracker = util.CursorDebugWrapper
+
+
+class DatabaseStatTracker(DatabaseStatTracker):
"""
Replacement for CursorDebugWrapper which outputs information as it happens.
"""
logger = None
-
+
def execute(self, sql, params=()):
formatted_sql = sql % (params if isinstance(params, dict) else tuple(params))
- if self.logger and (not settings.DEVSERVER_SQL_MIN_DURATION
- or duration > settings.DEVSERVER_SQL_MIN_DURATION):
+ if self.logger:
message = formatted_sql
if settings.DEVSERVER_TRUNCATE_SQL:
message = truncate_sql(message, aggregates=settings.DEVSERVER_TRUNCATE_AGGREGATES)
message = sqlparse.format(message, reindent=True, keyword_case='upper')
self.logger.debug(message)
-
+
start = datetime.now()
+
try:
- return self.cursor.execute(sql, params)
+ return super(DatabaseStatTracker, self).execute(sql, params)
finally:
stop = datetime.now()
duration = ms_from_timedelta(stop - start)
- # stacktrace = tidy_stacktrace(traceback.extract_stack())
- # template_info = None
- # # TODO: can probably move this into utils
- # cur_frame = sys._getframe().f_back
- # try:
- # while cur_frame is not None:
- # if cur_frame.f_code.co_name == 'render':
- # node = cur_frame.f_locals['self']
- # if isinstance(node, Node):
- # template_info = get_template_info(node.source)
- # break
- # cur_frame = cur_frame.f_back
- # except:
- # pass
- # del cur_frame
-
+
if self.logger and (not settings.DEVSERVER_SQL_MIN_DURATION
or duration > settings.DEVSERVER_SQL_MIN_DURATION):
if self.cursor.rowcount >= 0:
self.logger.debug('Found %s matching rows', self.cursor.rowcount, duration=duration)
-
- self.db.queries.append({
- 'sql': formatted_sql,
- 'time': duration,
- })
-
+
+ if not (debug_toolbar or settings.DEBUG):
+ self.db.queries.append({
+ 'sql': formatted_sql,
+ 'time': duration,
+ })
+
def executemany(self, sql, param_list):
start = datetime.now()
try:
- return self.cursor.executemany(sql, param_list)
+ return super(DatabaseStatTracker, self).executemany(sql, param_list)
finally:
stop = datetime.now()
duration = ms_from_timedelta(stop - start)
-
+
if self.logger:
message = sqlparse.format(sql, reindent=True, keyword_case='upper')
message = 'Executed %s times\n%s' % message
-
+
self.logger.debug(message, duration=duration)
self.logger.debug('Found %s matching rows', self.cursor.rowcount, duration=duration, id='query')
-
- self.db.queries.append({
- 'sql': '%s times: %s' % (len(param_list), sql),
- 'time': duration,
- })
+
+ if not (debug_toolbar or settings.DEBUG):
+ self.db.queries.append({
+ 'sql': '%s times: %s' % (len(param_list), sql),
+ 'time': duration,
+ })
+
class SQLRealTimeModule(DevServerModule):
"""
Outputs SQL queries as they happen.
"""
-
+
logger_name = 'sql'
-
+
def process_init(self, request):
- if not isinstance(util.CursorDebugWrapper, DatabaseStatTracker):
+ if not issubclass(util.CursorDebugWrapper, DatabaseStatTracker):
self.old_cursor = util.CursorDebugWrapper
util.CursorDebugWrapper = DatabaseStatTracker
DatabaseStatTracker.logger = self.logger
-
+
def process_complete(self, request):
- if isinstance(util.CursorDebugWrapper, DatabaseStatTracker):
+ if issubclass(util.CursorDebugWrapper, DatabaseStatTracker):
util.CursorDebugWrapper = self.old_cursor
+
class SQLSummaryModule(DevServerModule):
"""
Outputs a summary SQL queries.
"""
-
+
logger_name = 'sql'
-
+
def process_complete(self, request):
- num_queries = len(connection.queries)
+ queries = [
+ q for alias in connections
+ for q in connections[alias].queries
+ ]
+ num_queries = len(queries)
if num_queries:
- unique = set([s['sql'] for s in connection.queries])
+ unique = set([s['sql'] for s in queries])
self.logger.info('%(calls)s queries with %(dupes)s duplicates' % dict(
- calls = num_queries,
- dupes = num_queries - len(unique),
- ), duration=sum(float(c.get('time', 0)) for c in connection.queries))
-
+ calls=num_queries,
+ dupes=num_queries - len(unique),
+ ), duration=sum(float(c.get('time', 0)) for c in queries) * 1000)
View
3 devserver/settings.py
@@ -18,6 +18,9 @@
DEVSERVER_ACTIVE = False
DEVSERVER_AJAX_CONTENT_LENGTH = getattr(settings, 'DEVSERVER_AJAX_CONTENT_LENGTH', 300)
+DEVSERVER_AJAX_PRETTY_PRINT = getattr(settings, 'DEVSERVER_AJAX_PRETTY_PRINT', False)
# Minimum time a query must execute to be shown, value is in MS
DEVSERVER_SQL_MIN_DURATION = getattr(settings, 'DEVSERVER_SQL_MIN_DURATION', None)
+
+DEVSERVER_AUTO_PROFILE = getattr(settings, 'DEVSERVER_AUTO_PROFILE', False)
View
9 devserver/testcases.py
@@ -1,3 +1,5 @@
+import SocketServer
+
from django.conf import settings
from django.core.handlers.wsgi import WSGIHandler
from django.core.management import call_command
@@ -6,14 +8,13 @@
from devserver.utils.http import SlimWSGIRequestHandler
-import SocketServer
class ThreadedTestServerThread(TestServerThread):
def run(self):
try:
wsgi_handler = AdminMediaHandler(WSGIHandler())
server_address = (self.address, self.port)
-
+
class new(SocketServer.ThreadingMixIn, StoppableWSGIServer):
def __init__(self, *args, **kwargs):
StoppableWSGIServer.__init__(self, *args, **kwargs)
@@ -25,7 +26,7 @@ def __init__(self, *args, **kwargs):
self.error = e
self.started.set()
return
-
+
# Must do database stuff in this new thread if database in memory.
if settings.DATABASE_ENGINE == 'sqlite3' \
and (not settings.TEST_DATABASE_NAME or settings.TEST_DATABASE_NAME == ':memory:'):
@@ -37,4 +38,4 @@ def __init__(self, *args, **kwargs):
# Loop until we get a stop event.
while not self._stopevent.isSet():
- httpd.handle_request()
+ httpd.handle_request()
View
47 devserver/utils/http.py
@@ -1,33 +1,54 @@
+from datetime import datetime
+
from django.conf import settings
from django.core.servers.basehttp import WSGIRequestHandler
-from django.db import connection
+
+try:
+ from django.db import connections
+except ImportError:
+ # Django version < 1.2
+ from django.db import connection
+ connections = {'default': connection}
from devserver.utils.time import ms_from_timedelta
-from datetime import datetime
class SlimWSGIRequestHandler(WSGIRequestHandler):
"""
- Hides all requests that originate from ```MEDIA_URL`` as well as any
- request originating with a prefix included in ``DEVSERVER_IGNORED_PREFIXES``.
+ Hides all requests that originate from either ``STATIC_URL`` or ``MEDIA_URL``
+ as well as any request originating with a prefix included in
+ ``DEVSERVER_IGNORED_PREFIXES``.
"""
def handle(self, *args, **kwargs):
self._start_request = datetime.now()
return WSGIRequestHandler.handle(self, *args, **kwargs)
-
+
def log_message(self, format, *args):
duration = datetime.now() - self._start_request
-
- # if self.path.startswith(settings.MEDIA_URL):
- # return
+
+ env = self.get_environ()
+
+ for url in (getattr(settings, 'STATIC_URL', None), settings.MEDIA_URL):
+ if not url:
+ continue
+ if self.path.startswith(url):
+ return
+ elif url.startswith('http:'):
+ if ('http://%s%s' % (env['HTTP_HOST'], self.path)).startswith(url):
+ return
+
for path in getattr(settings, 'DEVSERVER_IGNORED_PREFIXES', []):
if self.path.startswith(path):
return
-
- format += " (time: %.2fms; sql: %.2fms (%dq))"
+
+ format += " (time: %.2fs; sql: %dms (%dq))"
+ queries = [
+ q for alias in connections
+ for q in connections[alias].queries
+ ]
args = list(args) + [
ms_from_timedelta(duration) / 1000,
- sum(float(c.get('time', 0)) for c in connection.queries),
- len(connection.queries),
+ sum(float(c.get('time', 0)) for c in queries) * 1000,
+ len(queries),
]
- return WSGIRequestHandler.log_message(self, format, *args)
+ return WSGIRequestHandler.log_message(self, format, *args)
View
5 devserver/utils/stack.py
@@ -3,11 +3,13 @@
import os.path
from django.conf import settings
+from django.views.debug import linebreak_iter
# Figure out some paths
django_path = os.path.realpath(os.path.dirname(django.__file__))
socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__))
+
def tidy_stacktrace(strace):
"""
Clean up stacktrace and remove all entries that:
@@ -26,6 +28,7 @@ def tidy_stacktrace(strace):
trace.append((s[0], s[1], s[2], s[3]))
return trace
+
def get_template_info(source, context_lines=3):
line = 0
upto = 0
@@ -58,4 +61,4 @@ def get_template_info(source, context_lines=3):
return {
'name': origin.name,
'context': context,
- }
+ }
View
26 devserver/utils/stats.py
@@ -7,14 +7,16 @@
from devserver.utils.time import ms_from_timedelta
+
__all__ = ('track', 'stats')
+
class StatCollection(object):
def __init__(self, *args, **kwargs):
super(StatCollection, self).__init__(*args, **kwargs)
self.reset()
-
- def run(self, func, key, *args, **kwargs):
+
+ def run(self, func, key, logger, *args, **kwargs):
"""Profile a function and store its information."""
start_time = datetime.now()
@@ -33,7 +35,7 @@ def run(self, func, key, *args, **kwargs):
row['time'] += this_time
if value is not None:
row['hits'] += 1
-
+
self.calls.setdefault(key, []).append({
'func': func,
'args': args,
@@ -47,9 +49,12 @@ def run(self, func, key, *args, **kwargs):
row['time'] += this_time
if value is not None:
row['hits'] += 1
-
+
+ if logger:
+ logger.debug('%s("%s") %s (%s)', func.__name__, args[0], 'Miss' if value is None else 'Hit', row['hits'], duration=this_time)
+
return value
-
+
def reset(self):
"""Reset the collection."""
self.grouped = {}
@@ -66,7 +71,7 @@ def get_total_hits(self, key):
return self.summary.get(key, {}).get('hits', 0)
def get_total_misses(self, key):
- return self.get_total_calls(key)-self.get_total_hits(key)
+ return self.get_total_calls(key) - self.get_total_hits(key)
def get_total_hits_for_function(self, key, func):
return self.grouped.get(key, {}).get(func.__name__, {}).get('hits', 0)
@@ -85,12 +90,13 @@ def get_calls(self, key):
stats = StatCollection()
-def track(func, key):
+
+def track(func, key, logger):
"""A decorator which handles tracking calls on a function."""
def wrapped(*args, **kwargs):
global stats
-
- return stats.run(func, key, *args, **kwargs)
+
+ return stats.run(func, key, logger, *args, **kwargs)
wrapped.__doc__ = func.__doc__
wrapped.__name__ = func.__name__
- return wrapped
+ return wrapped
View
1 devserver/utils/time.py
@@ -3,4 +3,3 @@ def ms_from_timedelta(td):
Given a timedelta object, returns a float representing milliseconds
"""
return (td.seconds * 1000) + (td.microseconds / 1000.0)
-
View
3 requirements.txt
@@ -1,4 +1,5 @@
Django>=1.1
sqlparse
werkzeug
-guppy
+guppy
+Dozer
View
2 setup.py
@@ -1,4 +1,3 @@
-import os
from setuptools import setup, find_packages
setup(name='django-devserver',
@@ -15,4 +14,5 @@
"Operating System :: OS Independent",
"Topic :: Software Development"
],
+ license="BSD",
)

0 comments on commit 7d3f515

Please sign in to comment.