├── .gitignore ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── README.rst ├── devserver ├── __init__.py ├── handlers.py ├── logger.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── runserver.py ├── middleware.py ├── models.py ├── modules │ ├── __init__.py │ ├── ajax.py │ ├── cache.py │ ├── profile.py │ ├── request.py │ └── sql.py ├── settings.py ├── testcases.py ├── tests.py └── utils │ ├── __init__.py │ ├── http.py │ ├── stack.py │ ├── stats.py │ └── time.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /dist 3 | /django_devserver.egg-info -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 0.8.0 2 | 3 | * Support for Django 1.6 4 | 5 | 0.3.1 6 | 7 | * Fixed a bug when --wsgi-app was not provided. 8 | 9 | 0.3 10 | 11 | * Added DEVSERVER_ARGS setting which is a list of CLI arguments to pass as defaults. 12 | * Added DEVSERVER_WSGI_MIDDLEWARE setting which is a list of additional WSGI middleware to apply. 13 | * Added --wsgi-app option to override the default WSGI application (does not inherit debug modules). 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 David Cramer and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the django-devserver nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup.py README.rst LICENSE 2 | global-exclude *~ 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ----- 2 | About 3 | ----- 4 | 5 | A drop in replacement for Django's built-in runserver command. Features include: 6 | 7 | * An extendable interface for handling things such as real-time logging. 8 | * Integration with the werkzeug interactive debugger. 9 | * Threaded (default) and multi-process development servers. 10 | * Ability to specify a WSGI application as your target environment. 11 | 12 | .. note:: django-devserver works on Django 1.3 and newer 13 | 14 | ------------ 15 | Installation 16 | ------------ 17 | 18 | To install the latest stable version:: 19 | 20 | pip install git+git://github.com/dcramer/django-devserver#egg=django-devserver 21 | 22 | 23 | django-devserver has some optional dependancies, which we highly recommend installing. 24 | 25 | * ``pip install sqlparse`` -- pretty SQL formatting 26 | * ``pip install werkzeug`` -- interactive debugger 27 | * ``pip install guppy`` -- tracks memory usage (required for MemoryUseModule) 28 | * ``pip install line_profiler`` -- does line-by-line profiling (required for LineProfilerModule) 29 | 30 | You will need to include ``devserver`` in your ``INSTALLED_APPS``:: 31 | 32 | INSTALLED_APPS = ( 33 | ... 34 | 'devserver', 35 | ) 36 | 37 | If you're using ``django.contrib.staticfiles`` or any other apps with management 38 | command ``runserver``, make sure to put ``devserver`` *above* any of them (or *below*, 39 | for ``Django<1.7``). Otherwise ``devserver`` will log an error, but it will fail to work 40 | properly. 41 | 42 | ----- 43 | Usage 44 | ----- 45 | 46 | Once installed, using the new runserver replacement is easy. You must specify verbosity of 0 to disable real-time log output:: 47 | 48 | python manage.py runserver 49 | 50 | Note: This will force ``settings.DEBUG`` to ``True``. 51 | 52 | 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. 53 | 54 | Additional CLI Options 55 | ~~~~~~~~~~~~~~~~~~~~~~ 56 | 57 | --werkzeug 58 | Tells Django to use the Werkzeug interactive debugger, instead of it's own. 59 | 60 | --forked 61 | Use a forking (multi-process) web server instead of threaded. 62 | 63 | --dozer 64 | Enable the dozer memory debugging middleware (at /_dozer) 65 | 66 | --wsgi-app 67 | Load the specified WSGI app as the server endpoint. 68 | 69 | Please see ``python manage.py runserver --help`` for more information additional options. 70 | 71 | Note: You may also use devserver's middleware outside of the management command:: 72 | 73 | MIDDLEWARE_CLASSES = ( 74 | 'devserver.middleware.DevServerMiddleware', 75 | ) 76 | 77 | ------------- 78 | Configuration 79 | ------------- 80 | 81 | The following options may be configured via your ``settings.py``: 82 | 83 | DEVSERVER_ARGS = [] 84 | Additional command line arguments to pass to the ``runserver`` command (as defaults). 85 | 86 | DEVSERVER_DEFAULT_ADDR = '127.0.0.1' 87 | The default address to bind to. 88 | 89 | DEVSERVER_DEFAULT_PORT = '8000' 90 | The default port to bind to. 91 | 92 | DEVSERVER_WSGI_MIDDLEWARE 93 | A list of additional WSGI middleware to apply to the ``runserver`` command. 94 | 95 | DEVSERVER_MODULES = [] 96 | A list of devserver modules to load. 97 | 98 | DEVSERVER_IGNORED_PREFIXES = ['/media', '/uploads'] 99 | 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) 100 | 101 | 102 | ------- 103 | Modules 104 | ------- 105 | 106 | django-devserver includes several modules by default, but is also extendable by 3rd party modules. This is done via the ``DEVSERVER_MODULES`` setting:: 107 | 108 | DEVSERVER_MODULES = ( 109 | 'devserver.modules.sql.SQLRealTimeModule', 110 | 'devserver.modules.sql.SQLSummaryModule', 111 | 'devserver.modules.profile.ProfileSummaryModule', 112 | 113 | # Modules not enabled by default 114 | 'devserver.modules.ajax.AjaxDumpModule', 115 | 'devserver.modules.profile.MemoryUseModule', 116 | 'devserver.modules.cache.CacheSummaryModule', 117 | 'devserver.modules.profile.LineProfilerModule', 118 | ) 119 | 120 | devserver.modules.sql.SQLRealTimeModule 121 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 122 | Outputs queries as they happen to the terminal, including time taken. 123 | 124 | Disable SQL query truncation (used in SQLRealTimeModule) with the ``DEVSERVER_TRUNCATE_SQL`` setting:: 125 | 126 | DEVSERVER_TRUNCATE_SQL = False 127 | 128 | Filter SQL queries with the ``DEVSERVER_FILTER_SQL`` setting:: 129 | 130 | DEVSERVER_FILTER_SQL = ( 131 | re.compile('djkombu_\w+'), # Filter all queries related to Celery 132 | ) 133 | 134 | devserver.modules.sql.SQLSummaryModule 135 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 136 | 137 | Outputs a summary of your SQL usage. 138 | 139 | devserver.modules.profile.ProfileSummaryModule 140 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 141 | Outputs a summary of the request performance. 142 | 143 | devserver.modules.profile.MemoryUseModule 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | Outputs a notice when memory use is increased (at the end of a request cycle). 146 | 147 | devserver.modules.profile.LineProfilerModule 148 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 149 | 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. 150 | 151 | An example of a decorated function:: 152 | 153 | @devserver_profile(follow=[foo, bar]) 154 | def home(request): 155 | result['foo'] = foo() 156 | result['bar'] = bar() 157 | 158 | When using the decorator, we recommend that rather than import the decoration directly from devserver that you have code somewhere in your project like:: 159 | 160 | try: 161 | if 'devserver' not in settings.INSTALLED_APPS: 162 | raise ImportError 163 | from devserver.modules.profile import devserver_profile 164 | except ImportError: 165 | from functools import wraps 166 | class devserver_profile(object): 167 | def __init__(self, *args, **kwargs): 168 | pass 169 | def __call__(self, func): 170 | def nothing(*args, **kwargs): 171 | return func(*args, **kwargs) 172 | return wraps(func)(nothing) 173 | 174 | By importing the decoration using this method, devserver_profile will be a pass through decoration if you aren't using devserver (eg in production) 175 | 176 | 177 | devserver.modules.cache.CacheSummaryModule 178 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 179 | 180 | Outputs a summary of your cache calls at the end of the request. 181 | 182 | devserver.modules.ajax.AjaxDumpModule 183 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 184 | 185 | Outputs the content of any AJAX responses 186 | 187 | Change the maximum response length to dump with the ``DEVSERVER_AJAX_CONTENT_LENGTH`` setting:: 188 | 189 | DEVSERVER_AJAX_CONTENT_LENGTH = 300 190 | 191 | devserver.modules.request.SessionInfoModule 192 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 193 | 194 | Outputs information about the current session and user. 195 | 196 | ---------------- 197 | Building Modules 198 | ---------------- 199 | 200 | Building modules in devserver is quite simple. In fact, it resembles the middleware API almost identically. 201 | 202 | Let's take a sample module, which simple tells us when a request has started, and when it has finished:: 203 | 204 | from devserver.modules import DevServerModule 205 | 206 | class UselessModule(DevServerModule): 207 | logger_name = 'useless' 208 | 209 | def process_request(self, request): 210 | self.logger.info('Request started') 211 | 212 | def process_response(self, request, response): 213 | self.logger.info('Request ended') 214 | 215 | There are additional arguments which may be sent to logger methods, such as ``duration``:: 216 | 217 | # duration is in milliseconds 218 | self.logger.info('message', duration=13.134) 219 | -------------------------------------------------------------------------------- /devserver/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | django-devserver 3 | ~~~~~~ 4 | 5 | `django-devserver ` is a package 6 | that aims to replace the built-in runserver command by providing additional 7 | functionality such as real-time SQL debugging. 8 | 9 | :copyright: 2010 by David Cramer 10 | """ 11 | 12 | __all__ = ('__version__', '__build__', '__docformat__', 'get_revision') 13 | __version__ = (0, 8, 0) 14 | __docformat__ = 'restructuredtext en' 15 | 16 | import os 17 | 18 | 19 | def _get_git_revision(path): 20 | revision_file = os.path.join(path, 'refs', 'heads', 'master') 21 | if not os.path.exists(revision_file): 22 | return None 23 | fh = open(revision_file, 'r') 24 | try: 25 | return fh.read().strip() 26 | finally: 27 | fh.close() 28 | 29 | 30 | def get_revision(): 31 | """ 32 | :returns: Revision number of this branch/checkout, if available. None if 33 | no revision number can be determined. 34 | """ 35 | package_dir = os.path.dirname(__file__) 36 | checkout_dir = os.path.normpath(os.path.join(package_dir, '..')) 37 | path = os.path.join(checkout_dir, '.git') 38 | if os.path.exists(path): 39 | return _get_git_revision(path) 40 | return None 41 | 42 | __build__ = get_revision() 43 | 44 | 45 | def get_version(): 46 | base = '.'.join(map(str, __version__)) 47 | if __build__: 48 | base = '%s (%s)' % (base, __build__) 49 | return base 50 | -------------------------------------------------------------------------------- /devserver/handlers.py: -------------------------------------------------------------------------------- 1 | from django.core.handlers.wsgi import WSGIHandler 2 | 3 | from devserver.middleware import DevServerMiddleware 4 | 5 | 6 | class DevServerHandler(WSGIHandler): 7 | def load_middleware(self): 8 | super(DevServerHandler, self).load_middleware() 9 | 10 | i = DevServerMiddleware() 11 | 12 | # TODO: verify this order is fine 13 | self._request_middleware.append(i.process_request) 14 | self._view_middleware.append(i.process_view) 15 | self._response_middleware.append(i.process_response) 16 | self._exception_middleware.append(i.process_exception) 17 | -------------------------------------------------------------------------------- /devserver/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import re 4 | import datetime 5 | 6 | from django.utils.encoding import smart_str 7 | from django.core.management.color import color_style 8 | from django.utils import termcolors 9 | 10 | 11 | _bash_colors = re.compile(r'\x1b\[[^m]*m') 12 | 13 | 14 | def strip_bash_colors(string): 15 | return _bash_colors.sub('', string) 16 | 17 | 18 | class GenericLogger(object): 19 | def __init__(self, module): 20 | self.module = module 21 | self.style = color_style() 22 | 23 | def log(self, message, *args, **kwargs): 24 | id = kwargs.pop('id', None) 25 | duration = kwargs.pop('duration', None) 26 | level = kwargs.pop('level', logging.INFO) 27 | 28 | tpl_bits = [] 29 | if id: 30 | tpl_bits.append(self.style.SQL_FIELD('[%s/%s]' % (self.module.logger_name, id))) 31 | else: 32 | tpl_bits.append(self.style.SQL_FIELD('[%s]' % self.module.logger_name)) 33 | if duration: 34 | tpl_bits.append(self.style.SQL_KEYWORD('(%dms)' % duration)) 35 | 36 | if args: 37 | message = message % args 38 | 39 | message = smart_str(message) 40 | 41 | if level == logging.ERROR: 42 | message = self.style.ERROR(message) 43 | elif level == logging.WARN: 44 | message = self.style.NOTICE(message) 45 | else: 46 | try: 47 | HTTP_INFO = self.style.HTTP_INFO 48 | except: 49 | HTTP_INFO = termcolors.make_style(fg='red') 50 | message = HTTP_INFO(message) 51 | 52 | tpl = ' '.join(tpl_bits) % dict( 53 | id=id, 54 | module=self.module.logger_name, 55 | asctime=datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 56 | ) 57 | 58 | indent = ' ' * (len(strip_bash_colors(tpl)) + 1) 59 | 60 | new_message = [] 61 | first = True 62 | for line in message.split('\n'): 63 | if first: 64 | new_message.append(line) 65 | else: 66 | new_message.append('%s%s' % (indent, line)) 67 | first = False 68 | 69 | message = '%s %s' % (tpl, '\n'.join(new_message)) 70 | 71 | sys.stdout.write(' ' + message + '\n') 72 | 73 | warn = lambda x, *a, **k: x.log(level=logging.WARN, *a, **k) 74 | info = lambda x, *a, **k: x.log(level=logging.INFO, *a, **k) 75 | debug = lambda x, *a, **k: x.log(level=logging.DEBUG, *a, **k) 76 | error = lambda x, *a, **k: x.log(level=logging.ERROR, *a, **k) 77 | critical = lambda x, *a, **k: x.log(level=logging.CRITICAL, *a, **k) 78 | fatal = lambda x, *a, **k: x.log(level=logging.FATAL, *a, **k) 79 | -------------------------------------------------------------------------------- /devserver/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-devserver/35f9abf601e6cc2ed3cdbb78fb60018d7bd5a48d/devserver/management/__init__.py -------------------------------------------------------------------------------- /devserver/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-devserver/35f9abf601e6cc2ed3cdbb78fb60018d7bd5a48d/devserver/management/commands/__init__.py -------------------------------------------------------------------------------- /devserver/management/commands/runserver.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.management.commands.runserver import Command as BaseCommand 3 | from django.core.management.base import CommandError, handle_default_options 4 | from django.core.servers.basehttp import WSGIServer 5 | from django.core.handlers.wsgi import WSGIHandler 6 | 7 | import os 8 | import sys 9 | import imp 10 | import errno 11 | import socket 12 | import SocketServer 13 | from optparse import make_option 14 | 15 | from devserver.handlers import DevServerHandler 16 | from devserver.utils.http import SlimWSGIRequestHandler 17 | 18 | try: 19 | from django.core.servers.basehttp import (WSGIServerException as 20 | wsgi_server_exc_cls) 21 | except ImportError: # Django 1.6 22 | wsgi_server_exc_cls = socket.error 23 | 24 | 25 | STATICFILES_APPS = ('django.contrib.staticfiles', 'staticfiles') 26 | 27 | 28 | def null_technical_500_response(request, exc_type, exc_value, tb): 29 | raise exc_type, exc_value, tb 30 | 31 | 32 | def run(addr, port, wsgi_handler, mixin=None, ipv6=False): 33 | if mixin: 34 | class new(mixin, WSGIServer): 35 | def __init__(self, *args, **kwargs): 36 | WSGIServer.__init__(self, *args, **kwargs) 37 | else: 38 | new = WSGIServer 39 | server_address = (addr, port) 40 | new.request_queue_size = 10 41 | httpd = new(server_address, SlimWSGIRequestHandler, ipv6=ipv6) 42 | httpd.set_app(wsgi_handler) 43 | httpd.serve_forever() 44 | 45 | 46 | class Command(BaseCommand): 47 | option_list = BaseCommand.option_list + ( 48 | make_option( 49 | '--werkzeug', action='store_true', dest='use_werkzeug', default=False, 50 | help='Tells Django to use the Werkzeug interactive debugger.'), 51 | make_option( 52 | '--forked', action='store_true', dest='use_forked', default=False, 53 | help='Use forking instead of threading for multiple web requests.'), 54 | make_option( 55 | '--dozer', action='store_true', dest='use_dozer', default=False, 56 | help='Enable the Dozer memory debugging middleware.'), 57 | make_option( 58 | '--wsgi-app', dest='wsgi_app', default=None, 59 | help='Load the specified WSGI app as the server endpoint.'), 60 | ) 61 | if any(map(lambda app: app in settings.INSTALLED_APPS, STATICFILES_APPS)): 62 | option_list += make_option( 63 | '--nostatic', dest='use_static_files', action='store_false', default=True, 64 | help='Tells Django to NOT automatically serve static files at STATIC_URL.'), 65 | 66 | help = "Starts a lightweight Web server for development which outputs additional debug information." 67 | args = '[optional port number, or ipaddr:port]' 68 | 69 | # Validation is called explicitly each time the server is reloaded. 70 | def __init__(self): 71 | # `requires_model_validation` is deprecated in favor of 72 | # `requires_system_checks`. If both options are present, an error is 73 | # raised. BaseCommand sets requires_system_checks in >= Django 1.7. 74 | if hasattr(self, 'requires_system_checks'): 75 | requires_system_checks = False 76 | else: 77 | requires_model_validation = False # Django < 1.7 78 | super(Command, self).__init__() 79 | 80 | def run_from_argv(self, argv): 81 | parser = self.create_parser(argv[0], argv[1]) 82 | default_args = getattr(settings, 'DEVSERVER_ARGS', None) 83 | if default_args: 84 | options, args = parser.parse_args(default_args) 85 | else: 86 | options = None 87 | 88 | options, args = parser.parse_args(argv[2:], options) 89 | 90 | handle_default_options(options) 91 | self.execute(*args, **options.__dict__) 92 | 93 | def handle(self, addrport='', *args, **options): 94 | if args: 95 | raise CommandError('Usage is runserver %s' % self.args) 96 | 97 | if not addrport: 98 | addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '127.0.0.1') 99 | port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', '8000') 100 | addrport = '%s:%s' % (addr, port) 101 | 102 | return super(Command, self).handle(addrport=addrport, *args, **options) 103 | 104 | def get_handler(self, *args, **options): 105 | if int(options['verbosity']) < 1: 106 | handler = WSGIHandler() 107 | else: 108 | handler = DevServerHandler() 109 | 110 | # AdminMediaHandler is removed in Django 1.5 111 | # Add it only when it avialable. 112 | try: 113 | from django.core.servers.basehttp import AdminMediaHandler 114 | except ImportError: 115 | pass 116 | else: 117 | handler = AdminMediaHandler( 118 | handler, options['admin_media_path']) 119 | 120 | if 'django.contrib.staticfiles' in settings.INSTALLED_APPS and options['use_static_files']: 121 | from django.contrib.staticfiles.handlers import StaticFilesHandler 122 | handler = StaticFilesHandler(handler) 123 | 124 | return handler 125 | 126 | def inner_run(self, *args, **options): 127 | # Flag the server as active 128 | from devserver import settings 129 | import devserver 130 | settings.DEVSERVER_ACTIVE = True 131 | settings.DEBUG = True 132 | 133 | from django.conf import settings 134 | from django.utils import translation 135 | 136 | shutdown_message = options.get('shutdown_message', '') 137 | use_werkzeug = options.get('use_werkzeug', False) 138 | quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C' 139 | wsgi_app = options.get('wsgi_app', None) 140 | 141 | if use_werkzeug: 142 | try: 143 | from werkzeug import run_simple, DebuggedApplication 144 | except ImportError, e: 145 | self.stderr.write("WARNING: Unable to initialize werkzeug: %s\n" % e) 146 | use_werkzeug = False 147 | else: 148 | from django.views import debug 149 | debug.technical_500_response = null_technical_500_response 150 | 151 | self.stdout.write("Validating models...\n\n") 152 | self.validate(display_num_errors=True) 153 | self.stdout.write(( 154 | "Django version %(version)s, using settings %(settings)r\n" 155 | "Running django-devserver %(devserver_version)s\n" 156 | "%(server_model)s %(server_type)s server is running at http://%(addr)s:%(port)s/\n" 157 | "Quit the server with %(quit_command)s.\n" 158 | ) % { 159 | "server_type": use_werkzeug and 'werkzeug' or 'Django', 160 | "server_model": options['use_forked'] and 'Forked' or 'Threaded', 161 | "version": self.get_version(), 162 | "devserver_version": devserver.get_version(), 163 | "settings": settings.SETTINGS_MODULE, 164 | "addr": self._raw_ipv6 and '[%s]' % self.addr or self.addr, 165 | "port": self.port, 166 | "quit_command": quit_command, 167 | }) 168 | 169 | # django.core.management.base forces the locale to en-us. We should 170 | # set it up correctly for the first request (particularly important 171 | # in the "--noreload" case). 172 | translation.activate(settings.LANGUAGE_CODE) 173 | 174 | app = self.get_handler(*args, **options) 175 | if wsgi_app: 176 | self.stdout.write("Using WSGI application %r\n" % wsgi_app) 177 | if os.path.exists(os.path.abspath(wsgi_app)): 178 | # load from file 179 | app = imp.load_source('wsgi_app', os.path.abspath(wsgi_app)).application 180 | else: 181 | try: 182 | app = __import__(wsgi_app, {}, {}, ['application']).application 183 | except (ImportError, AttributeError): 184 | raise 185 | 186 | if options['use_forked']: 187 | mixin = SocketServer.ForkingMixIn 188 | else: 189 | mixin = SocketServer.ThreadingMixIn 190 | 191 | middleware = getattr(settings, 'DEVSERVER_WSGI_MIDDLEWARE', []) 192 | for middleware in middleware: 193 | module, class_name = middleware.rsplit('.', 1) 194 | app = getattr(__import__(module, {}, {}, [class_name]), class_name)(app) 195 | 196 | if options['use_dozer']: 197 | from dozer import Dozer 198 | app = Dozer(app) 199 | 200 | try: 201 | if use_werkzeug: 202 | run_simple( 203 | self.addr, int(self.port), DebuggedApplication(app, True), 204 | use_reloader=False, use_debugger=True) 205 | else: 206 | run(self.addr, int(self.port), app, mixin, ipv6=self.use_ipv6) 207 | 208 | except wsgi_server_exc_cls, e: 209 | # Use helpful error messages instead of ugly tracebacks. 210 | ERRORS = { 211 | errno.EACCES: "You don't have permission to access that port.", 212 | errno.EADDRINUSE: "That port is already in use.", 213 | errno.EADDRNOTAVAIL: "That IP address can't be assigned-to.", 214 | } 215 | if not isinstance(e, socket.error): # Django < 1.6 216 | ERRORS[13] = ERRORS.pop(errno.EACCES) 217 | ERRORS[98] = ERRORS.pop(errno.EADDRINUSE) 218 | ERRORS[99] = ERRORS.pop(errno.EADDRNOTAVAIL) 219 | 220 | try: 221 | if not isinstance(e, socket.error): # Django < 1.6 222 | error_text = ERRORS[e.args[0].args[0]] 223 | else: 224 | error_text = ERRORS[e.errno] 225 | except (AttributeError, KeyError): 226 | error_text = str(e) 227 | sys.stderr.write(self.style.ERROR("Error: %s" % error_text) + '\n') 228 | # Need to use an OS exit because sys.exit doesn't work in a thread 229 | os._exit(1) 230 | 231 | except KeyboardInterrupt: 232 | if shutdown_message: 233 | self.stdout.write("%s\n" % shutdown_message) 234 | sys.exit(0) 235 | -------------------------------------------------------------------------------- /devserver/middleware.py: -------------------------------------------------------------------------------- 1 | from devserver.models import MODULES 2 | 3 | 4 | class DevServerMiddleware(object): 5 | def should_process(self, request): 6 | from django.conf import settings 7 | 8 | if getattr(settings, 'STATIC_URL', None) and request.build_absolute_uri().startswith(request.build_absolute_uri(settings.STATIC_URL)): 9 | return False 10 | 11 | if settings.MEDIA_URL and request.build_absolute_uri().startswith(request.build_absolute_uri(settings.MEDIA_URL)): 12 | return False 13 | 14 | if getattr(settings, 'ADMIN_MEDIA_PREFIX', None) and request.path.startswith(settings.ADMIN_MEDIA_PREFIX): 15 | return False 16 | 17 | if request.path == '/favicon.ico': 18 | return False 19 | 20 | for path in getattr(settings, 'DEVSERVER_IGNORED_PREFIXES', []): 21 | if request.path.startswith(path): 22 | return False 23 | 24 | return True 25 | 26 | def process_request(self, request): 27 | # Set a sentinel value which process_response can use to abort when 28 | # another middleware app short-circuits processing: 29 | request._devserver_active = True 30 | 31 | self.process_init(request) 32 | 33 | if self.should_process(request): 34 | for mod in MODULES: 35 | mod.process_request(request) 36 | 37 | def process_response(self, request, response): 38 | # If this isn't set, it usually means that another middleware layer 39 | # has returned an HttpResponse and the following middleware won't see 40 | # the request. This happens most commonly with redirections - see 41 | # https://github.com/dcramer/django-devserver/issues/28 for details: 42 | if not getattr(request, "_devserver_active", False): 43 | return response 44 | 45 | if self.should_process(request): 46 | for mod in MODULES: 47 | mod.process_response(request, response) 48 | 49 | self.process_complete(request) 50 | 51 | return response 52 | 53 | def process_exception(self, request, exception): 54 | if self.should_process(request): 55 | for mod in MODULES: 56 | mod.process_exception(request, exception) 57 | 58 | def process_view(self, request, view_func, view_args, view_kwargs): 59 | if self.should_process(request): 60 | for mod in MODULES: 61 | mod.process_view(request, view_func, view_args, view_kwargs) 62 | #return view_func(request, *view_args, **view_kwargs) 63 | 64 | def process_init(self, request): 65 | from devserver.utils.stats import stats 66 | 67 | stats.reset() 68 | 69 | if self.should_process(request): 70 | for mod in MODULES: 71 | mod.process_init(request) 72 | 73 | def process_complete(self, request): 74 | if self.should_process(request): 75 | for mod in MODULES: 76 | mod.process_complete(request) 77 | -------------------------------------------------------------------------------- /devserver/models.py: -------------------------------------------------------------------------------- 1 | import django 2 | from django.core import exceptions 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | from devserver.logger import GenericLogger 6 | import logging 7 | 8 | 9 | MODULES = [] 10 | 11 | 12 | def check_installed_apps_configuration(): 13 | """ 14 | Check the app is put in correct order in INSTALLED_APPS 15 | 16 | django.contrib.staticfiles runserver command is likely to 17 | override devserver management command if put in wrong order. 18 | 19 | Django had reversed order of management commands collection prior to 1.7 20 | https://code.djangoproject.com/ticket/16599 21 | """ 22 | from django.conf import settings 23 | try: 24 | staticfiles_index = settings.INSTALLED_APPS.index('django.contrib.staticfiles') 25 | devserver_index = settings.INSTALLED_APPS.index('devserver') 26 | except ValueError: 27 | pass 28 | else: 29 | latest_app_overrides = django.VERSION < (1, 7) 30 | if devserver_index < staticfiles_index and latest_app_overrides: 31 | logging.error( 32 | 'Put "devserver" below "django.contrib.staticfiles" in INSTALLED_APPS to make it work') 33 | elif devserver_index > staticfiles_index and not latest_app_overrides: 34 | logging.error( 35 | 'Put "devserver" above "django.contrib.staticfiles" in INSTALLED_APPS to make it work') 36 | 37 | 38 | def load_modules(): 39 | global MODULES 40 | 41 | MODULES = [] 42 | 43 | from devserver import settings 44 | 45 | for path in settings.DEVSERVER_MODULES: 46 | try: 47 | name, class_name = path.rsplit('.', 1) 48 | except ValueError: 49 | raise exceptions.ImproperlyConfigured, '%s isn\'t a devserver module' % path 50 | 51 | try: 52 | module = __import__(name, {}, {}, ['']) 53 | except ImportError, e: 54 | raise exceptions.ImproperlyConfigured, 'Error importing devserver module %s: "%s"' % (name, e) 55 | 56 | try: 57 | cls = getattr(module, class_name) 58 | except AttributeError: 59 | raise exceptions.ImproperlyConfigured, 'Error importing devserver module "%s" does not define a "%s" class' % (name, class_name) 60 | 61 | try: 62 | instance = cls(GenericLogger(cls)) 63 | except: 64 | raise # Bubble up problem loading panel 65 | 66 | MODULES.append(instance) 67 | 68 | if not MODULES: 69 | check_installed_apps_configuration() 70 | load_modules() 71 | -------------------------------------------------------------------------------- /devserver/modules/__init__.py: -------------------------------------------------------------------------------- 1 | class DevServerModule(object): 2 | """ 3 | Functions a lot like middleware, except that it does not accept any return values. 4 | """ 5 | logger_name = 'generic' 6 | 7 | def __init__(self, logger): 8 | self.logger = logger 9 | 10 | def process_request(self, request): 11 | pass 12 | 13 | def process_response(self, request, response): 14 | pass 15 | 16 | def process_exception(self, request, exception): 17 | pass 18 | 19 | def process_view(self, request, view_func, view_args, view_kwargs): 20 | pass 21 | 22 | def process_init(self, request): 23 | pass 24 | 25 | def process_complete(self, request): 26 | pass 27 | -------------------------------------------------------------------------------- /devserver/modules/ajax.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from devserver.modules import DevServerModule 4 | from devserver import settings 5 | 6 | 7 | class AjaxDumpModule(DevServerModule): 8 | """ 9 | Dumps the content of all AJAX responses. 10 | """ 11 | 12 | logger_name = 'ajax' 13 | 14 | def process_response(self, request, response): 15 | if request.is_ajax(): 16 | # Let's do a quick test to see what kind of response we have 17 | if len(response.content) < settings.DEVSERVER_AJAX_CONTENT_LENGTH: 18 | content = response.content 19 | if settings.DEVSERVER_AJAX_PRETTY_PRINT: 20 | content = json.dumps(json.loads(content), indent=4) 21 | self.logger.info(content) 22 | -------------------------------------------------------------------------------- /devserver/modules/cache.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | 3 | from devserver.modules import DevServerModule 4 | 5 | 6 | class CacheSummaryModule(DevServerModule): 7 | """ 8 | Outputs a summary of cache events once a response is ready. 9 | """ 10 | real_time = False 11 | 12 | logger_name = 'cache' 13 | 14 | attrs_to_track = ['set', 'get', 'delete', 'add', 'get_many'] 15 | 16 | def process_init(self, request): 17 | from devserver.utils.stats import track 18 | 19 | # save our current attributes 20 | self.old = dict((k, getattr(cache, k)) for k in self.attrs_to_track) 21 | 22 | for k in self.attrs_to_track: 23 | setattr(cache, k, track(getattr(cache, k), 'cache', self.logger if self.real_time else None)) 24 | 25 | def process_complete(self, request): 26 | from devserver.utils.stats import stats 27 | 28 | calls = stats.get_total_calls('cache') 29 | hits = stats.get_total_hits('cache') 30 | misses = stats.get_total_misses_for_function('cache', cache.get) + stats.get_total_misses_for_function('cache', cache.get_many) 31 | 32 | if calls and (hits or misses): 33 | ratio = int(hits / float(misses + hits) * 100) 34 | else: 35 | ratio = 100 36 | 37 | if not self.real_time: 38 | self.logger.info('%(calls)s calls made with a %(ratio)d%% hit percentage (%(misses)s misses)' % dict( 39 | calls=calls, 40 | ratio=ratio, 41 | hits=hits, 42 | misses=misses, 43 | ), duration=stats.get_total_time('cache')) 44 | 45 | # set our attributes back to their defaults 46 | for k, v in self.old.iteritems(): 47 | setattr(cache, k, v) 48 | 49 | 50 | class CacheRealTimeModule(CacheSummaryModule): 51 | real_time = True 52 | -------------------------------------------------------------------------------- /devserver/modules/profile.py: -------------------------------------------------------------------------------- 1 | from devserver.modules import DevServerModule 2 | from devserver.utils.time import ms_from_timedelta 3 | from devserver.settings import DEVSERVER_AUTO_PROFILE 4 | 5 | from datetime import datetime 6 | 7 | import functools 8 | import gc 9 | 10 | 11 | class ProfileSummaryModule(DevServerModule): 12 | """ 13 | Outputs a summary of cache events once a response is ready. 14 | """ 15 | 16 | logger_name = 'profile' 17 | 18 | def process_init(self, request): 19 | self.start = datetime.now() 20 | 21 | def process_complete(self, request): 22 | duration = datetime.now() - self.start 23 | 24 | self.logger.info('Total time to render was %.2fs', ms_from_timedelta(duration) / 1000) 25 | 26 | 27 | class LeftOversModule(DevServerModule): 28 | """ 29 | Outputs a summary of events the garbage collector couldn't handle. 30 | """ 31 | # TODO: Not even sure this is correct, but the its a general idea 32 | 33 | logger_name = 'profile' 34 | 35 | def process_init(self, request): 36 | gc.enable() 37 | gc.set_debug(gc.DEBUG_SAVEALL) 38 | 39 | def process_complete(self, request): 40 | gc.collect() 41 | self.logger.info('%s objects left in garbage', len(gc.garbage)) 42 | 43 | from django.template.defaultfilters import filesizeformat 44 | 45 | try: 46 | from guppy import hpy 47 | except ImportError: 48 | import warnings 49 | 50 | class MemoryUseModule(DevServerModule): 51 | def __new__(cls, *args, **kwargs): 52 | warnings.warn('MemoryUseModule requires guppy to be installed.') 53 | return super(MemoryUseModule, cls).__new__(cls) 54 | else: 55 | class MemoryUseModule(DevServerModule): 56 | """ 57 | Outputs a summary of memory usage of the course of a request. 58 | """ 59 | logger_name = 'profile' 60 | 61 | def __init__(self, request): 62 | super(MemoryUseModule, self).__init__(request) 63 | self.hpy = hpy() 64 | self.oldh = self.hpy.heap() 65 | self.logger.info('heap size is %s', filesizeformat(self.oldh.size)) 66 | 67 | def process_complete(self, request): 68 | newh = self.hpy.heap() 69 | alloch = newh - self.oldh 70 | dealloch = self.oldh - newh 71 | self.oldh = newh 72 | self.logger.info('%s allocated, %s deallocated, heap size is %s', *map(filesizeformat, [alloch.size, dealloch.size, newh.size])) 73 | 74 | try: 75 | from line_profiler import LineProfiler 76 | except ImportError: 77 | import warnings 78 | 79 | class LineProfilerModule(DevServerModule): 80 | 81 | def __new__(cls, *args, **kwargs): 82 | warnings.warn('LineProfilerModule requires line_profiler to be installed.') 83 | return super(LineProfilerModule, cls).__new__(cls) 84 | 85 | class devserver_profile(object): 86 | def __init__(self, follow=[]): 87 | pass 88 | 89 | def __call__(self, func): 90 | return func 91 | else: 92 | class LineProfilerModule(DevServerModule): 93 | """ 94 | Outputs a Line by Line profile of any @devserver_profile'd functions that were run 95 | """ 96 | logger_name = 'profile' 97 | 98 | def process_view(self, request, view_func, view_args, view_kwargs): 99 | request.devserver_profiler = LineProfiler() 100 | request.devserver_profiler_run = False 101 | if (DEVSERVER_AUTO_PROFILE): 102 | _unwrap_closure_and_profile(request.devserver_profiler, view_func) 103 | request.devserver_profiler.enable_by_count() 104 | 105 | def process_complete(self, request): 106 | if hasattr(request, 'devserver_profiler_run') and (DEVSERVER_AUTO_PROFILE or request.devserver_profiler_run): 107 | from cStringIO import StringIO 108 | out = StringIO() 109 | if (DEVSERVER_AUTO_PROFILE): 110 | request.devserver_profiler.disable_by_count() 111 | request.devserver_profiler.print_stats(stream=out) 112 | self.logger.info(out.getvalue()) 113 | 114 | def _unwrap_closure_and_profile(profiler, func): 115 | if not hasattr(func, 'func_code'): 116 | return 117 | profiler.add_function(func) 118 | if func.func_closure: 119 | for cell in func.func_closure: 120 | if hasattr(cell.cell_contents, 'func_code'): 121 | _unwrap_closure_and_profile(profiler, cell.cell_contents) 122 | 123 | class devserver_profile(object): 124 | def __init__(self, follow=[]): 125 | self.follow = follow 126 | 127 | def __call__(self, func): 128 | def profiled_func(*args, **kwargs): 129 | request = args[0] 130 | if hasattr(request, 'request'): 131 | # We're decorating a Django class-based-view and the first argument is actually self: 132 | request = args[1] 133 | 134 | try: 135 | request.devserver_profiler.add_function(func) 136 | request.devserver_profiler_run = True 137 | for f in self.follow: 138 | request.devserver_profiler.add_function(f) 139 | request.devserver_profiler.enable_by_count() 140 | return func(*args, **kwargs) 141 | finally: 142 | request.devserver_profiler.disable_by_count() 143 | 144 | return functools.wraps(func)(profiled_func) 145 | -------------------------------------------------------------------------------- /devserver/modules/request.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | from devserver.modules import DevServerModule 4 | 5 | 6 | class SessionInfoModule(DevServerModule): 7 | """ 8 | Displays information about the currently authenticated user and session. 9 | """ 10 | 11 | logger_name = 'session' 12 | 13 | def process_request(self, request): 14 | self.has_session = bool(getattr(request, 'session', False)) 15 | if self.has_session is not None: 16 | self._save = request.session.save 17 | self.session = request.session 18 | request.session.save = self.handle_session_save 19 | 20 | def process_response(self, request, response): 21 | if getattr(self, 'has_session', False): 22 | if getattr(request, 'user', None) and request.user.is_authenticated(): 23 | user = '%s (id:%s)' % (request.user.username, request.user.pk) 24 | else: 25 | user = '(Anonymous)' 26 | self.logger.info('Session %s authenticated by %s', request.session.session_key, user) 27 | request.session.save = self._save 28 | self._save = None 29 | self.session = None 30 | self.has_session = False 31 | 32 | def handle_session_save(self, *args, **kwargs): 33 | self._save(*args, **kwargs) 34 | self.logger.info('Session %s has been saved.', self.session.session_key) 35 | 36 | 37 | class RequestDumpModule(DevServerModule): 38 | """ 39 | Dumps the request headers and variables. 40 | """ 41 | 42 | logger_name = 'request' 43 | 44 | def process_request(self, request): 45 | 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'])) 46 | for var, val in request.META.items(): 47 | if var.startswith('HTTP_'): 48 | var = var[5:].replace('_', '-').title() 49 | req += '%s: %s\n' % (self.logger.style.SQL_KEYWORD(var), val) 50 | if request.META['CONTENT_LENGTH']: 51 | req += '%s: %s\n' % (self.logger.style.SQL_KEYWORD('Content-Length'), request.META['CONTENT_LENGTH']) 52 | if request.POST: 53 | req += '\n%s\n' % self.logger.style.HTTP_INFO(urllib.urlencode(dict((k, v.encode('utf8')) for k, v in request.POST.items()))) 54 | if request.FILES: 55 | req += '\n%s\n' % self.logger.style.HTTP_NOT_MODIFIED(urllib.urlencode(request.FILES)) 56 | self.logger.info('Full request:\n%s', req) 57 | 58 | class ResponseDumpModule(DevServerModule): 59 | """ 60 | Dumps the request headers and variables. 61 | """ 62 | 63 | logger_name = 'response' 64 | 65 | def process_response(self, request, response): 66 | res = self.logger.style.SQL_FIELD('Status code: %s\n' % response.status_code) 67 | res += '\n'.join(['%s: %s' % (self.logger.style.SQL_FIELD(k), v) 68 | for k, v in response._headers.values()]) 69 | self.logger.info('Full response:\n%s', res) 70 | -------------------------------------------------------------------------------- /devserver/modules/sql.py: -------------------------------------------------------------------------------- 1 | """ 2 | Based on initial work from django-debug-toolbar 3 | """ 4 | import re 5 | 6 | from datetime import datetime 7 | 8 | try: 9 | from django.db import connections 10 | except ImportError: 11 | # Django version < 1.2 12 | from django.db import connection 13 | connections = {'default': connection} 14 | 15 | try: 16 | from django.db.backends import utils # renamed in django 1.7 17 | except ImportError: 18 | from django.db.backends import util as utils # removed in django 1.9 19 | from django.conf import settings as django_settings 20 | #from django.template import Node 21 | 22 | from devserver.modules import DevServerModule 23 | #from devserver.utils.stack import tidy_stacktrace, get_template_info 24 | from devserver.utils.time import ms_from_timedelta 25 | from devserver import settings 26 | 27 | try: 28 | import sqlparse 29 | except ImportError: 30 | class sqlparse: 31 | @staticmethod 32 | def format(text, *args, **kwargs): 33 | return text 34 | 35 | 36 | _sql_fields_re = re.compile(r'SELECT .*? FROM') 37 | _sql_aggregates_re = re.compile(r'SELECT .*?(COUNT|SUM|AVERAGE|MIN|MAX).*? FROM') 38 | 39 | 40 | def truncate_sql(sql, aggregates=True): 41 | if not aggregates and _sql_aggregates_re.match(sql): 42 | return sql 43 | return _sql_fields_re.sub('SELECT ... FROM', sql) 44 | 45 | # # TODO:This should be set in the toolbar loader as a default and panels should 46 | # # get a copy of the toolbar object with access to its config dictionary 47 | # SQL_WARNING_THRESHOLD = getattr(settings, 'DEVSERVER_CONFIG', {}) \ 48 | # .get('SQL_WARNING_THRESHOLD', 500) 49 | 50 | try: 51 | from debug_toolbar.panels.sql import DatabaseStatTracker 52 | debug_toolbar = True 53 | except ImportError: 54 | debug_toolbar = False 55 | import django 56 | version = float('.'.join([str(x) for x in django.VERSION[:2]])) 57 | if version >= 1.6: 58 | DatabaseStatTracker = utils.CursorWrapper 59 | else: 60 | DatabaseStatTracker = utils.CursorDebugWrapper 61 | 62 | 63 | class DatabaseStatTracker(DatabaseStatTracker): 64 | """ 65 | Replacement for CursorDebugWrapper which outputs information as it happens. 66 | """ 67 | logger = None 68 | 69 | def execute(self, sql, params=()): 70 | formatted_sql = sql % (params if isinstance(params, dict) else tuple(params)) 71 | if self.logger: 72 | message = formatted_sql 73 | if settings.DEVSERVER_FILTER_SQL: 74 | if any(filter_.search(message) for filter_ in settings.DEVSERVER_FILTER_SQL): 75 | message = None 76 | if message is not None: 77 | if settings.DEVSERVER_TRUNCATE_SQL: 78 | message = truncate_sql(message, aggregates=settings.DEVSERVER_TRUNCATE_AGGREGATES) 79 | message = sqlparse.format(message, reindent=True, keyword_case='upper') 80 | self.logger.debug(message) 81 | 82 | start = datetime.now() 83 | 84 | try: 85 | return super(DatabaseStatTracker, self).execute(sql, params) 86 | finally: 87 | stop = datetime.now() 88 | duration = ms_from_timedelta(stop - start) 89 | 90 | if self.logger and (not settings.DEVSERVER_SQL_MIN_DURATION 91 | or duration > settings.DEVSERVER_SQL_MIN_DURATION): 92 | if self.cursor.rowcount >= 0 and message is not None: 93 | self.logger.debug('Found %s matching rows', self.cursor.rowcount, duration=duration) 94 | 95 | if not (debug_toolbar or django_settings.DEBUG): 96 | self.db.queries.append({ 97 | 'sql': formatted_sql, 98 | 'time': duration, 99 | }) 100 | 101 | def executemany(self, sql, param_list): 102 | start = datetime.now() 103 | try: 104 | return super(DatabaseStatTracker, self).executemany(sql, param_list) 105 | finally: 106 | stop = datetime.now() 107 | duration = ms_from_timedelta(stop - start) 108 | 109 | if self.logger: 110 | message = sqlparse.format(sql, reindent=True, keyword_case='upper') 111 | 112 | message = 'Executed %s times\n' % message 113 | 114 | self.logger.debug(message, duration=duration) 115 | self.logger.debug('Found %s matching rows', self.cursor.rowcount, duration=duration, id='query') 116 | 117 | if not (debug_toolbar or settings.DEBUG): 118 | self.db.queries.append({ 119 | 'sql': '%s times: %s' % (len(param_list), sql), 120 | 'time': duration, 121 | }) 122 | 123 | 124 | class SQLRealTimeModule(DevServerModule): 125 | """ 126 | Outputs SQL queries as they happen. 127 | """ 128 | 129 | logger_name = 'sql' 130 | 131 | def process_init(self, request): 132 | if not issubclass(utils.CursorDebugWrapper, DatabaseStatTracker): 133 | self.old_cursor = utils.CursorDebugWrapper 134 | utils.CursorDebugWrapper = DatabaseStatTracker 135 | DatabaseStatTracker.logger = self.logger 136 | 137 | def process_complete(self, request): 138 | if issubclass(utils.CursorDebugWrapper, DatabaseStatTracker): 139 | utils.CursorDebugWrapper = self.old_cursor 140 | 141 | 142 | class SQLSummaryModule(DevServerModule): 143 | """ 144 | Outputs a summary SQL queries. 145 | """ 146 | 147 | logger_name = 'sql' 148 | 149 | def process_complete(self, request): 150 | queries = [ 151 | q for alias in connections 152 | for q in connections[alias].queries 153 | ] 154 | num_queries = len(queries) 155 | if num_queries: 156 | unique = set([s['sql'] for s in queries]) 157 | self.logger.info('%(calls)s queries with %(dupes)s duplicates' % dict( 158 | calls=num_queries, 159 | dupes=num_queries - len(unique), 160 | ), duration=sum(float(c.get('time', 0)) for c in queries) * 1000) 161 | -------------------------------------------------------------------------------- /devserver/settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | DEVSERVER_MODULES = getattr(settings, 'DEVSERVER_MODULES', ( 4 | 'devserver.modules.sql.SQLRealTimeModule', 5 | # 'devserver.modules.sql.SQLSummaryModule', 6 | # 'devserver.modules.profile.ProfileSummaryModule', 7 | # 'devserver.modules.request.SessionInfoModule', 8 | # 'devserver.modules.profile.MemoryUseModule', 9 | # 'devserver.modules.profile.LeftOversModule', 10 | # 'devserver.modules.cache.CacheSummaryModule', 11 | )) 12 | 13 | DEVSERVER_FILTER_SQL = getattr(settings, 'DEVSERVER_FILTER_SQL', False) 14 | DEVSERVER_TRUNCATE_SQL = getattr(settings, 'DEVSERVER_TRUNCATE_SQL', True) 15 | 16 | DEVSERVER_TRUNCATE_AGGREGATES = getattr(settings, 'DEVSERVER_TRUNCATE_AGGREGATES', getattr(settings, 'DEVSERVER_TRUNCATE_AGGREGATES', False)) 17 | 18 | # This variable gets set to True when we're running the devserver 19 | DEVSERVER_ACTIVE = False 20 | 21 | DEVSERVER_AJAX_CONTENT_LENGTH = getattr(settings, 'DEVSERVER_AJAX_CONTENT_LENGTH', 300) 22 | DEVSERVER_AJAX_PRETTY_PRINT = getattr(settings, 'DEVSERVER_AJAX_PRETTY_PRINT', False) 23 | 24 | # Minimum time a query must execute to be shown, value is in MS 25 | DEVSERVER_SQL_MIN_DURATION = getattr(settings, 'DEVSERVER_SQL_MIN_DURATION', None) 26 | 27 | DEVSERVER_AUTO_PROFILE = getattr(settings, 'DEVSERVER_AUTO_PROFILE', False) 28 | -------------------------------------------------------------------------------- /devserver/testcases.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import SocketServer 3 | import threading 4 | 5 | from django.conf import settings 6 | from django.core.handlers.wsgi import WSGIHandler 7 | from django.core.management import call_command 8 | from django.core.servers.basehttp import WSGIServer 9 | 10 | from devserver.utils.http import SlimWSGIRequestHandler 11 | 12 | try: 13 | from django.core.servers.basehttp import (WSGIServerException as 14 | wsgi_server_exc_cls) 15 | except ImportError: # Django 1.6 16 | wsgi_server_exc_cls = socket.error 17 | 18 | 19 | class StoppableWSGIServer(WSGIServer): 20 | """WSGIServer with short timeout, so that server thread can stop this server.""" 21 | 22 | def server_bind(self): 23 | """Sets timeout to 1 second.""" 24 | WSGIServer.server_bind(self) 25 | self.socket.settimeout(1) 26 | 27 | def get_request(self): 28 | """Checks for timeout when getting request.""" 29 | try: 30 | sock, address = self.socket.accept() 31 | sock.settimeout(None) 32 | return (sock, address) 33 | except socket.timeout: 34 | raise 35 | 36 | 37 | class ThreadedTestServerThread(threading.Thread): 38 | """Thread for running a http server while tests are running.""" 39 | 40 | def __init__(self, address, port): 41 | self.address = address 42 | self.port = port 43 | self._stopevent = threading.Event() 44 | self.started = threading.Event() 45 | self.error = None 46 | super(ThreadedTestServerThread, self).__init__() 47 | 48 | def _should_loaddata(self): 49 | # Must do database stuff in this new thread if database in memory. 50 | if not hasattr(self, 'fixtures'): 51 | return False 52 | if settings.DATABASE_ENGINE != 'sqlite3': 53 | return False 54 | if settings.TEST_DATABASE_NAME and settings.TEST_DATABASE_NAME != ':memory:': 55 | return False 56 | return True 57 | 58 | def run(self): 59 | """Sets up test server and database and loops over handling http requests.""" 60 | # AdminMediaHandler was removed in Django 1.5; use it only when available. 61 | handler = WSGIHandler() 62 | try: 63 | from django.core.servers.basehttp import AdminMediaHandler 64 | handler = AdminMediaHandler(handler) 65 | except ImportError: 66 | pass 67 | 68 | try: 69 | server_address = (self.address, self.port) 70 | 71 | class new(SocketServer.ThreadingMixIn, StoppableWSGIServer): 72 | def __init__(self, *args, **kwargs): 73 | StoppableWSGIServer.__init__(self, *args, **kwargs) 74 | 75 | httpd = new(server_address, SlimWSGIRequestHandler) 76 | httpd.set_app(handler) 77 | self.started.set() 78 | except wsgi_server_exc_cls, e: 79 | self.error = e 80 | self.started.set() 81 | return 82 | 83 | if self._should_loaddata(): 84 | # We have to use this slightly awkward syntax due to the fact 85 | # that we're using *args and **kwargs together. 86 | call_command('loaddata', *self.fixtures, **{'verbosity': 0}) 87 | 88 | # Loop until we get a stop event. 89 | while not self._stopevent.isSet(): 90 | httpd.handle_request() 91 | 92 | def join(self, timeout=None): 93 | """Stop the thread and wait for it to finish.""" 94 | self._stopevent.set() 95 | threading.Thread.join(self, timeout) 96 | -------------------------------------------------------------------------------- /devserver/tests.py: -------------------------------------------------------------------------------- 1 | # TODO -------------------------------------------------------------------------------- /devserver/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/django-devserver/35f9abf601e6cc2ed3cdbb78fb60018d7bd5a48d/devserver/utils/__init__.py -------------------------------------------------------------------------------- /devserver/utils/http.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from django.conf import settings 4 | from django.core.servers.basehttp import WSGIRequestHandler 5 | 6 | try: 7 | from django.db import connections 8 | except ImportError: 9 | # Django version < 1.2 10 | from django.db import connection 11 | connections = {'default': connection} 12 | 13 | from devserver.utils.time import ms_from_timedelta 14 | 15 | 16 | class SlimWSGIRequestHandler(WSGIRequestHandler): 17 | """ 18 | Hides all requests that originate from either ``STATIC_URL`` or ``MEDIA_URL`` 19 | as well as any request originating with a prefix included in 20 | ``DEVSERVER_IGNORED_PREFIXES``. 21 | """ 22 | def handle(self, *args, **kwargs): 23 | self._start_request = datetime.now() 24 | return WSGIRequestHandler.handle(self, *args, **kwargs) 25 | 26 | def get_environ(self): 27 | env = super(SlimWSGIRequestHandler, self).get_environ() 28 | env['REMOTE_PORT'] = self.client_address[-1] 29 | return env 30 | 31 | def log_message(self, format, *args): 32 | duration = datetime.now() - self._start_request 33 | 34 | env = self.get_environ() 35 | 36 | for url in (getattr(settings, 'STATIC_URL', None), settings.MEDIA_URL): 37 | if not url: 38 | continue 39 | if self.path.startswith(url): 40 | return 41 | elif url.startswith('http:'): 42 | if ('http://%s%s' % (env['HTTP_HOST'], self.path)).startswith(url): 43 | return 44 | 45 | for path in getattr(settings, 'DEVSERVER_IGNORED_PREFIXES', []): 46 | if self.path.startswith(path): 47 | return 48 | 49 | format += " (time: %.2fs; sql: %dms (%dq))" 50 | queries = [ 51 | q for alias in connections 52 | for q in connections[alias].queries 53 | ] 54 | args = list(args) + [ 55 | ms_from_timedelta(duration) / 1000, 56 | sum(float(c.get('time', 0)) for c in queries) * 1000, 57 | len(queries), 58 | ] 59 | return WSGIRequestHandler.log_message(self, format, *args) 60 | -------------------------------------------------------------------------------- /devserver/utils/stack.py: -------------------------------------------------------------------------------- 1 | import django 2 | import SocketServer 3 | import os.path 4 | 5 | from django.conf import settings 6 | from django.views.debug import linebreak_iter 7 | 8 | # Figure out some paths 9 | django_path = os.path.realpath(os.path.dirname(django.__file__)) 10 | socketserver_path = os.path.realpath(os.path.dirname(SocketServer.__file__)) 11 | 12 | 13 | def tidy_stacktrace(strace): 14 | """ 15 | Clean up stacktrace and remove all entries that: 16 | 1. Are part of Django (except contrib apps) 17 | 2. Are part of SocketServer (used by Django's dev server) 18 | 3. Are the last entry (which is part of our stacktracing code) 19 | """ 20 | trace = [] 21 | for s in strace[:-1]: 22 | s_path = os.path.realpath(s[0]) 23 | if getattr(settings, 'DEVSERVER_CONFIG', {}).get('HIDE_DJANGO_SQL', True) \ 24 | and django_path in s_path and not 'django/contrib' in s_path: 25 | continue 26 | if socketserver_path in s_path: 27 | continue 28 | trace.append((s[0], s[1], s[2], s[3])) 29 | return trace 30 | 31 | 32 | def get_template_info(source, context_lines=3): 33 | line = 0 34 | upto = 0 35 | source_lines = [] 36 | before = during = after = "" 37 | 38 | origin, (start, end) = source 39 | template_source = origin.reload() 40 | 41 | for num, next in enumerate(linebreak_iter(template_source)): 42 | if start >= upto and end <= next: 43 | line = num 44 | before = template_source[upto:start] 45 | during = template_source[start:end] 46 | after = template_source[end:next] 47 | source_lines.append((num, template_source[upto:next])) 48 | upto = next 49 | 50 | top = max(1, line - context_lines) 51 | bottom = min(len(source_lines), line + 1 + context_lines) 52 | 53 | context = [] 54 | for num, content in source_lines[top:bottom]: 55 | context.append({ 56 | 'num': num, 57 | 'content': content, 58 | 'highlight': (num == line), 59 | }) 60 | 61 | return { 62 | 'name': origin.name, 63 | 'context': context, 64 | } 65 | -------------------------------------------------------------------------------- /devserver/utils/stats.py: -------------------------------------------------------------------------------- 1 | try: 2 | from threading import local 3 | except ImportError: 4 | from django.utils._threading_local import local 5 | 6 | from datetime import datetime 7 | 8 | from devserver.utils.time import ms_from_timedelta 9 | 10 | 11 | __all__ = ('track', 'stats') 12 | 13 | 14 | class StatCollection(object): 15 | def __init__(self, *args, **kwargs): 16 | super(StatCollection, self).__init__(*args, **kwargs) 17 | self.reset() 18 | 19 | def run(self, func, key, logger, *args, **kwargs): 20 | """Profile a function and store its information.""" 21 | 22 | start_time = datetime.now() 23 | value = func(*args, **kwargs) 24 | end_time = datetime.now() 25 | this_time = ms_from_timedelta(end_time - start_time) 26 | values = { 27 | 'args': args, 28 | 'kwargs': kwargs, 29 | 'count': 0, 30 | 'hits': 0, 31 | 'time': 0.0 32 | } 33 | row = self.grouped.setdefault(key, {}).setdefault(func.__name__, values) 34 | row['count'] += 1 35 | row['time'] += this_time 36 | if value is not None: 37 | row['hits'] += 1 38 | 39 | self.calls.setdefault(key, []).append({ 40 | 'func': func, 41 | 'args': args, 42 | 'kwargs': kwargs, 43 | 'time': this_time, 44 | 'hit': value is not None, 45 | #'stack': [s[1:] for s in inspect.stack()[2:]], 46 | }) 47 | row = self.summary.setdefault(key, {'count': 0, 'time': 0.0, 'hits': 0}) 48 | row['count'] += 1 49 | row['time'] += this_time 50 | if value is not None: 51 | row['hits'] += 1 52 | 53 | if logger: 54 | logger.debug('%s("%s") %s (%s)', func.__name__, args[0], 'Miss' if value is None else 'Hit', row['hits'], duration=this_time) 55 | 56 | return value 57 | 58 | def reset(self): 59 | """Reset the collection.""" 60 | self.grouped = {} 61 | self.calls = {} 62 | self.summary = {} 63 | 64 | def get_total_time(self, key): 65 | return self.summary.get(key, {}).get('time', 0) 66 | 67 | def get_total_calls(self, key): 68 | return self.summary.get(key, {}).get('count', 0) 69 | 70 | def get_total_hits(self, key): 71 | return self.summary.get(key, {}).get('hits', 0) 72 | 73 | def get_total_misses(self, key): 74 | return self.get_total_calls(key) - self.get_total_hits(key) 75 | 76 | def get_total_hits_for_function(self, key, func): 77 | return self.grouped.get(key, {}).get(func.__name__, {}).get('hits', 0) 78 | 79 | def get_total_calls_for_function(self, key, func): 80 | return self.grouped.get(key, {}).get(func.__name__, {}).get('count', 0) 81 | 82 | def get_total_misses_for_function(self, key, func): 83 | return self.get_total_calls_for_function(key, func) - self.get_total_hits_for_function(key, func) 84 | 85 | def get_total_time_for_function(self, key, func): 86 | return self.grouped.get(key, {}).get(func.__name__, {}).get('time', 0) 87 | 88 | def get_calls(self, key): 89 | return self.calls.get(key, []) 90 | 91 | stats = StatCollection() 92 | 93 | 94 | def track(func, key, logger): 95 | """A decorator which handles tracking calls on a function.""" 96 | def wrapped(*args, **kwargs): 97 | global stats 98 | 99 | return stats.run(func, key, logger, *args, **kwargs) 100 | wrapped.__doc__ = func.__doc__ 101 | wrapped.__name__ = func.__name__ 102 | return wrapped 103 | -------------------------------------------------------------------------------- /devserver/utils/time.py: -------------------------------------------------------------------------------- 1 | def ms_from_timedelta(td): 2 | """ 3 | Given a timedelta object, returns a float representing milliseconds 4 | """ 5 | return (td.seconds * 1000) + (td.microseconds / 1000.0) 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.1 2 | sqlparse 3 | werkzeug 4 | guppy 5 | Dozer -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='django-devserver', 5 | version=".".join(map(str, __import__("devserver").__version__)), 6 | description='Drop-in replacement for Django\'s runserver', 7 | author='David Cramer', 8 | author_email='dcramer@gmail.com', 9 | url='http://github.com/dcramer/django-devserver', 10 | packages=find_packages(), 11 | classifiers=[ 12 | "Framework :: Django", 13 | "Intended Audience :: Developers", 14 | "Intended Audience :: System Administrators", 15 | "Operating System :: OS Independent", 16 | "Topic :: Software Development" 17 | ], 18 | license="BSD", 19 | ) 20 | --------------------------------------------------------------------------------