├── django_pdb ├── models.py ├── templatetags │ ├── __init__.py │ └── pdb.py ├── management │ ├── commands │ │ ├── __init__.py │ │ ├── test.py │ │ └── runserver.py │ └── __init__.py ├── apps.py ├── __init__.py ├── compat.py ├── utils.py ├── middleware.py └── testrunners.py ├── testproject ├── testproject │ ├── __init__.py │ ├── testapp │ │ ├── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ └── test.html │ │ ├── views.py │ │ └── tests.py │ ├── urls.py │ └── settings.py └── manage.py ├── .gitignore ├── CONTRIBUTING.md ├── CHANGELOG.txt ├── setup.py └── README.rst /django_pdb/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_pdb/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testproject/testproject/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_pdb/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testproject/testproject/testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.db 3 | *.egg-info 4 | *~ 5 | env 6 | build 7 | dist 8 | -------------------------------------------------------------------------------- /testproject/testproject/testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /testproject/testproject/testapp/templates/test.html: -------------------------------------------------------------------------------- 1 | {% load pdb %} 2 | 3 |
The variable: {{ variable|pdb }}
4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | I don't actively use this package anymore, and am not currently accepting any pull requests apart from clear bugs or Django compatibility issues. 2 | -------------------------------------------------------------------------------- /django_pdb/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class DjangoPdbConfig(AppConfig): 7 | name = 'django_pdb' 8 | verbose_name = "Django Pdb" 9 | -------------------------------------------------------------------------------- /testproject/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os, sys 3 | 4 | if __name__ == "__main__": 5 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings") 6 | 7 | from django.core.management import execute_from_command_line 8 | 9 | execute_from_command_line(sys.argv) 10 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.6.2 - Compat for middleware check 2 | 0.6.1 - Python 3 compatibility fix 3 | 0.6.0 - Version 0.6 (Django 1.10 support) 4 | 0.4.0 - Py3k support 5 | 0.2.3 - Update README with better info re. conflicting apps. 6 | 0.2.2 - Fix long_description in PyPI. 7 | 0.2.1 - Add empty models.py. 8 | 0.2.0 - Add ipdb support. 9 | -------------------------------------------------------------------------------- /testproject/testproject/testapp/views.py: -------------------------------------------------------------------------------- 1 | """ 2 | A dummy view to demonstrate using 'manage.py runserver --pdb' 3 | """ 4 | from django.http import HttpResponse 5 | from django.shortcuts import render 6 | 7 | 8 | def myview(request): 9 | a = 1 10 | b = 2 11 | c = 3 12 | return HttpResponse('Hello, you.', content_type='text/plain') 13 | 14 | def filter_view(request): 15 | variable = "I'm the variable" 16 | return render(request, 'test.html', {"variable": variable}) 17 | -------------------------------------------------------------------------------- /django_pdb/templatetags/pdb.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django_pdb.utils import has_ipdb 3 | 4 | 5 | register = template.Library() 6 | 7 | @register.filter 8 | def pdb(element): 9 | from django_pdb.utils import get_pdb_set_trace 10 | get_pdb_set_trace()() 11 | return element 12 | 13 | 14 | @register.filter 15 | def ipdb(element): 16 | if has_ipdb(): 17 | from ipdb import set_trace 18 | else: 19 | from django_pdb.utils import get_pdb_set_trace 20 | get_pdb_set_trace()() 21 | set_trace() 22 | return element 23 | -------------------------------------------------------------------------------- /testproject/testproject/testapp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class SimpleTest(TestCase): 5 | """ 6 | A couple of dummy tests to demonstrate 'manage.py test --pdb'. 7 | """ 8 | 9 | def test_error(self): 10 | """ 11 | Tests that 1 + 1 always equals 4. 12 | """ 13 | a = 1 14 | b = 2 15 | c = 3 16 | one_plus_one = four 17 | 18 | def test_failure(self): 19 | """ 20 | Tests that 1 + 1 always equals 4. 21 | """ 22 | a = 1 23 | b = 2 24 | c = 3 25 | self.assertEqual(1 + 1, 4) 26 | -------------------------------------------------------------------------------- /testproject/testproject/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Example: 9 | (r'^$', 'testproject.testapp.views.myview'), 10 | (r'^filter/$', 'testproject.testapp.views.filter_view'), 11 | 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # (r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # (r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /django_pdb/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from django.conf import settings 3 | __version__ = '0.6.2' 4 | 5 | POST_MORTEM = getattr(settings, 'POST_MORTEM', False) 6 | DEBUG = getattr(settings, 'DEBUG', False) 7 | 8 | if DEBUG and POST_MORTEM == True: 9 | from django.views import debug 10 | def runpdb(request, exc_type, exc_value, tb): 11 | import sys 12 | try: 13 | import ipdb 14 | except ImportError: 15 | import pdb as ipdb 16 | p = ipdb 17 | print('Exception occured: {}, {}'.format(exc_type, exc_value), file=sys.stderr) 18 | p.post_mortem(tb) 19 | debug.technical_500_response = runpdb 20 | 21 | default_app_config = 'django_pdb.apps.DjangoPdbConfig' 22 | -------------------------------------------------------------------------------- /django_pdb/management/__init__.py: -------------------------------------------------------------------------------- 1 | from django.core import management 2 | from django.core.management import find_commands 3 | try: 4 | # Django > 1.7 5 | from django.utils.module_loading import import_module 6 | except ImportError: 7 | # Django < 1.7 8 | from django.utils.importlib import import_module 9 | 10 | from ..compat import load_management_modules 11 | 12 | # A cache of loaded commands, so that call_command 13 | # doesn't have to reload every time it's called. 14 | _parent_commands = None 15 | 16 | 17 | def get_parent_commands(): 18 | """ 19 | Returns a dictionary mapping command names to their callback applications. 20 | 21 | This function returns only callback applications above this 22 | application in the INSTALLED_APPS stack. 23 | """ 24 | global _parent_commands 25 | if _parent_commands is None: 26 | django_path = management.__path__ 27 | _parent_commands = dict([(name, 'django.core') 28 | for name in find_commands(django_path[0])]) 29 | 30 | load_management_modules(_parent_commands) 31 | 32 | # Reset the Django management cache 33 | management._commands = None 34 | 35 | return _parent_commands 36 | 37 | 38 | def load_parent_command(name): 39 | """ 40 | Given a command name, returns the Command class instance that is 41 | the above the current application. 42 | 43 | (ImportError, AttributeError) are allowed to propagate. 44 | """ 45 | app_name = get_parent_commands()[name] 46 | module = import_module('%s.management.commands.%s' % (app_name, name)) 47 | return module.Command 48 | -------------------------------------------------------------------------------- /django_pdb/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from django.core.management import find_commands 6 | 7 | try: 8 | from django_pdb.apps import DjangoPdbConfig 9 | from django.apps import apps 10 | except ImportError as e: 11 | # So django < 1.7 12 | from django.core.management import find_management_module 13 | import django_pdb.management 14 | 15 | def load_management_modules(commands): 16 | # Find the installed apps 17 | try: 18 | from django.conf import settings 19 | _apps = settings.INSTALLED_APPS 20 | except (AttributeError, EnvironmentError, ImportError): 21 | _apps = [] 22 | 23 | # Find and load the management module for each installed app above 24 | # this one. 25 | for app_name in _apps: 26 | try: 27 | path = find_management_module(app_name) 28 | if path == django_pdb.management.__path__[0]: 29 | # Found this app 30 | break 31 | 32 | commands.update( 33 | dict([(name, app_name) for name in find_commands(path)]) 34 | ) 35 | except ImportError: 36 | pass # No management module - ignore this app 37 | else: 38 | def load_management_modules(commands): 39 | app_configs = [app_config for app_config in apps.get_app_configs() if app_config.name != DjangoPdbConfig.name] 40 | for app_config in reversed(app_configs): 41 | path = os.path.join(app_config.path, 'management') 42 | commands.update({name: app_config.name for name in find_commands(path)}) 43 | -------------------------------------------------------------------------------- /django_pdb/utils.py: -------------------------------------------------------------------------------- 1 | def has_ipdb(): 2 | try: 3 | import ipdb 4 | import IPython 5 | return True 6 | except ImportError: 7 | return False 8 | 9 | 10 | def get_ipdb(): 11 | def_colors = get_def_colors() 12 | try: 13 | import ipdb 14 | from ipdb import __main__ 15 | return ipdb.__main__.Pdb(def_colors) 16 | except ImportError: # old versions of ipdb 17 | return ipdb.Pdb(def_colors) 18 | 19 | 20 | def get_pdb_set_trace(): 21 | # for the templatetags because the file is named 'pdb' and that cause an importation conflict 22 | from pdb import set_trace 23 | return set_trace 24 | 25 | 26 | def get_def_colors(): 27 | # Inspirated in https://github.com/gotcha/ipdb/blob/master/ipdb/__main__.py 28 | def_colors = 'Linux' 29 | import IPython 30 | if IPython.__version__ > '0.10.2': 31 | from IPython.core.debugger import Pdb 32 | try: 33 | get_ipython 34 | except NameError: 35 | from IPython.frontend.terminal.embed import InteractiveShellEmbed 36 | ipshell = InteractiveShellEmbed() 37 | def_colors = ipshell.colors 38 | else: 39 | try: 40 | def_colors = get_ipython.im_self.colors 41 | except AttributeError: 42 | def_colors = get_ipython.__self__.colors 43 | else: 44 | from IPython.Debugger import Pdb 45 | from IPython.Shell import IPShell 46 | from IPython import ipapi 47 | ip = ipapi.get() 48 | if ip is None: 49 | IPShell(argv=['']) 50 | ip = ipapi.get() 51 | def_colors = ip.options.colors 52 | return def_colors 53 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env/python 2 | # -*- coding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | import os 6 | import re 7 | 8 | 9 | def abs_path(relative_path): 10 | """ 11 | Given a path relative to this directory return an absolute path. 12 | """ 13 | base_path = os.path.abspath(os.path.dirname(__file__)) 14 | return os.path.join(base_path, relative_path) 15 | 16 | 17 | def get_version(relative_path): 18 | """ 19 | Return version given package's path. 20 | """ 21 | data = open(os.path.join(abs_path(relative_path), '__init__.py')).read() 22 | return re.search(r"__version__ = '([^']+)'", data).group(1) 23 | 24 | 25 | def get_long_description(): 26 | """ 27 | Return the contents of the README file. 28 | """ 29 | try: 30 | return open(abs_path('README.rst')).read() 31 | except: 32 | pass # Required to install using pip (won't have README then) 33 | 34 | 35 | setup( 36 | name='django-pdb', 37 | version=get_version('django_pdb'), 38 | description='Easier pdb debugging for Django', 39 | long_description=get_long_description(), 40 | author='Tom Christie', 41 | author_email='tom@tomchristie.com', 42 | url='https://github.com/tomchristie/django-pdb', 43 | packages=('django_pdb', 44 | 'django_pdb.management', 45 | 'django_pdb.management.commands', 46 | 'django_pdb.templatetags'), 47 | license='Public Domain', 48 | classifiers=[ 49 | 'Programming Language :: Python', 50 | 'Programming Language :: Python :: 2.6', 51 | 'Programming Language :: Python :: 2.7', 52 | 'Programming Language :: Python :: 3', 53 | 'Programming Language :: Python :: 3.3', 54 | 'Programming Language :: Python :: 3.4', 55 | 'Development Status :: 3 - Alpha', 56 | 'Intended Audience :: Developers', 57 | 'License :: Public Domain', 58 | 'Framework :: Django', 59 | 'Operating System :: OS Independent', 60 | ], 61 | ) 62 | -------------------------------------------------------------------------------- /django_pdb/management/commands/test.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | import sys 3 | 4 | from django import VERSION as DJANGO_VERSION 5 | from django.core.management.commands import test 6 | 7 | from django_pdb.management import load_parent_command 8 | from django_pdb.testrunners import make_suite_runner 9 | 10 | 11 | # Provide a Command class so that Django knows what will handle 12 | # things. This module does not override it, so it just needs to find 13 | # the parent Command. 14 | Command = load_parent_command('test') 15 | 16 | 17 | def patch_test_command(Command): 18 | """ 19 | Monkeypatches Django's TestCommand so that it chooses to use 20 | ipdb or pdb, allowing subclasses to inherit from it and wrap its 21 | behaviour. 22 | """ 23 | extra_options = [ 24 | ('--pdb', 25 | dict(action='store_true', dest='pdb', default=False, 26 | help='Drop into pdb shell on test errors or failures.')), 27 | ('--ipdb', 28 | dict(action='store_true', dest='ipdb', default=False, 29 | help='Drop into ipdb shell on test errors or failures.')), 30 | ] 31 | 32 | if DJANGO_VERSION >= (1, 8): 33 | # option_list is depecated since django 1.8 because optparse 34 | # is replaced by argsparse. Override add_arguements() to add 35 | # the extra pdb and ipdb options 36 | def add_arguments(self, parser): 37 | self._add_arguments(parser) 38 | for name, kwargs in extra_options: 39 | parser.add_argument(name, **kwargs) 40 | Command._add_arguments = Command.add_arguments 41 | Command.add_arguments = add_arguments 42 | else: 43 | Command.option_list += type(Command.option_list)([ 44 | make_option(name, **kwargs) for name, kwargs in extra_options 45 | ]) 46 | 47 | def handle(self, *test_labels, **options): 48 | """ 49 | If --pdb is set on the command line ignore the default test runner 50 | use the pdb test runner instead. 51 | """ 52 | pdb = options.pop('pdb') 53 | ipdb = options.pop('ipdb') 54 | 55 | if pdb or ipdb: 56 | options['verbosity'] = int(options.get('verbosity', 1)) 57 | options['interactive'] = options.get('interactive', True) 58 | options['failfast'] = options.get('failfast', False) 59 | 60 | TestRunner = self.get_runner(use_ipdb=ipdb) 61 | test_runner = TestRunner(**options) 62 | failures = test_runner.run_tests(test_labels) 63 | 64 | if failures: 65 | sys.exit(bool(failures)) 66 | 67 | else: 68 | self._handle(*test_labels, **options) 69 | 70 | Command._handle = Command.handle 71 | Command.handle = handle 72 | 73 | def get_runner(self, use_ipdb, suite_runner=None): 74 | return make_suite_runner(use_ipdb=use_ipdb, suite_runner=suite_runner) 75 | 76 | Command.get_runner = get_runner 77 | 78 | patch_test_command(test.Command) 79 | -------------------------------------------------------------------------------- /django_pdb/middleware.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import inspect 4 | import os 5 | import pdb 6 | import sys 7 | 8 | from django.conf import settings 9 | from django.core.exceptions import MiddlewareNotUsed 10 | 11 | from django_pdb.utils import get_ipdb, has_ipdb 12 | 13 | 14 | try: 15 | # MiddlewareMixin offers compatibility with both MIDDLEWARE and the old MIDDLEWARE_CLASSES. 16 | from django.utils.deprecation import MiddlewareMixin 17 | parent = MiddlewareMixin 18 | except ImportError: 19 | parent = object 20 | 21 | 22 | class PdbMiddleware(parent): 23 | """ 24 | Middleware to break into pdb at the start of views. 25 | 26 | If `always_break` is set, due to `runserver --pdb` this will break 27 | into pdb at the start of every view. 28 | 29 | Otherwise it will break into pdb at the start of the view if 30 | a 'pdb' GET parameter is set on the request url. 31 | """ 32 | 33 | always_break = False 34 | 35 | def __init__(self, get_response=None, debug_only=True): 36 | """ 37 | If debug_only is True, this middleware removes itself 38 | unless settings.DEBUG is also True. Otherwise, this middleware 39 | is always active. 40 | """ 41 | self.get_response = get_response 42 | if debug_only and not settings.DEBUG: 43 | raise MiddlewareNotUsed() 44 | 45 | def get_type_pdb(self, request): 46 | type_pdb = None 47 | if self.always_break: 48 | type_pdb = self.always_break 49 | elif request.GET.get('pdb', None) is not None: 50 | type_pdb = 'pdb' 51 | elif request.GET.get('ipdb', None) is not None: 52 | type_pdb = 'ipdb' 53 | return type_pdb 54 | 55 | def process_view(self, request, view_func, view_args, view_kwargs): 56 | """ 57 | If running with '--pdb', set a breakpoint at the start 58 | of each of each view before it gets called. 59 | """ 60 | # Skip out unless using `runserver --pdb`, 61 | # or `pdb` is in the command line parameters 62 | type_pdb = self.get_type_pdb(request) 63 | if not type_pdb: 64 | return 65 | 66 | filename = inspect.getsourcefile(view_func) 67 | basename = os.path.basename(filename) 68 | dirname = os.path.basename(os.path.dirname(filename)) 69 | lines, lineno = inspect.getsourcelines(view_func) 70 | temporary = True 71 | cond = None 72 | funcname = view_func.__name__ 73 | 74 | print() 75 | print('{} {}'.format(request.method, request.get_full_path())) 76 | print('function "{}" in {}/{}:{}'.format(funcname, 77 | dirname, basename, lineno)) 78 | print('args: {}'.format(view_args)) 79 | print('kwargs: {}'.format(view_kwargs)) 80 | print() 81 | 82 | if type_pdb == 'ipdb' and has_ipdb(): 83 | p = get_ipdb() 84 | else: 85 | if not type_pdb == 'pdb': 86 | print('You do not install ipdb or ipython module') 87 | p = pdb.Pdb() 88 | p.reset() 89 | p.set_break(filename, lineno + 1, temporary, cond, funcname) 90 | sys.settrace(p.trace_dispatch) 91 | -------------------------------------------------------------------------------- /django_pdb/management/commands/runserver.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import pdb 5 | 6 | from django import VERSION as DJANGO_VERSION 7 | 8 | from django_pdb.management import load_parent_command 9 | from django_pdb.middleware import PdbMiddleware 10 | 11 | from optparse import make_option 12 | from django_pdb.utils import has_ipdb 13 | from django.views import debug 14 | 15 | 16 | RunServerCommand = load_parent_command('runserver') 17 | 18 | extra_options = ( 19 | ('--pdb', 20 | dict(action='store_true', dest='pdb', default=False, 21 | help='Drop into pdb shell on at the start of any view.')), 22 | ('--ipdb', 23 | dict(action='store_true', dest='ipdb', default=False, 24 | help='Drop into ipdb shell on at the start of any view.')), 25 | ('--pm', 26 | dict(action='store_true', dest='pm', default=False, 27 | help='Drop into ipdb shell if an exception is raised in a view.')), 28 | ) 29 | 30 | 31 | class Command(RunServerCommand): 32 | """ 33 | Identical to Django's standard 'runserver' management command, 34 | except that it also adds support for a '--pdb' option. 35 | """ 36 | 37 | if DJANGO_VERSION >= (1, 8): 38 | # option_list is depecated since django 1.8 because optparse 39 | # is replaced by argsparse. Override add_arguements() to add 40 | # the extra pdb and ipdb options 41 | def add_arguments(self, parser): 42 | super(Command, self).add_arguments(parser) 43 | for name, kwargs in extra_options: 44 | parser.add_argument(name, **kwargs) 45 | else: 46 | option_list = RunServerCommand.option_list + tuple( 47 | make_option(name, **kwargs) for name, kwargs in extra_options 48 | ) 49 | 50 | def handle(self, *args, **options): 51 | # Add pdb middleware, if --pdb is specified, or if we're in DEBUG mode 52 | from django.conf import settings 53 | 54 | pdb_option = options.pop('pdb') 55 | ipdb_option = options.pop('ipdb') 56 | 57 | pdb_middleware = 'django_pdb.middleware.PdbMiddleware' 58 | middleware = (settings.MIDDLEWARE 59 | if hasattr(settings, 'MIDDLEWARE') 60 | else settings.MIDDLEWARE_CLASSES) 61 | if ((pdb_option or settings.DEBUG) 62 | and pdb_middleware not in middleware): 63 | middleware += (pdb_middleware,) 64 | 65 | self.pm = options.pop('pm') 66 | if self.pm: 67 | debug.technical_500_response = self.reraise 68 | 69 | # If --pdb is specified then always break at the start of views. 70 | # Otherwise break only if a 'pdb' query parameter is set in the url. 71 | if pdb_option: 72 | PdbMiddleware.always_break = 'pdb' 73 | elif ipdb_option: 74 | PdbMiddleware.always_break = 'ipdb' 75 | 76 | super(Command, self).handle(*args, **options) 77 | 78 | def reraise(self, request, exc_type, exc_value, tb): 79 | if has_ipdb(): 80 | import ipdb 81 | p = ipdb 82 | else: 83 | p = pdb 84 | if self.pm: 85 | print( 86 | "Exception occured: %s, %s" % (exc_type, exc_value), 87 | file=sys.stderr) 88 | p.post_mortem(tb) 89 | else: 90 | raise 91 | -------------------------------------------------------------------------------- /django_pdb/testrunners.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | 3 | from django.test.utils import get_runner 4 | 5 | import unittest 6 | from django_pdb.utils import has_ipdb 7 | 8 | 9 | class ExceptionTestResultMixin(object): 10 | """ 11 | A mixin class that can be added to any test result class. 12 | Drops into pdb on test errors/failures. 13 | """ 14 | pdb_type = 'pdb' 15 | 16 | def get_pdb(self): 17 | if self.pdb_type == 'ipdb' and has_ipdb(): 18 | import ipdb 19 | return ipdb 20 | return pdb 21 | 22 | def addError(self, test, err): 23 | super(ExceptionTestResultMixin, self).addError(test, err) 24 | exctype, value, tb = err 25 | 26 | self.stream.writeln() 27 | self.stream.writeln(self.separator1) 28 | self.stream.writeln(">>> %s" % (self.getDescription(test))) 29 | self.stream.writeln(self.separator2) 30 | self.stream.writeln(self._exc_info_to_string(err, test).rstrip()) 31 | self.stream.writeln(self.separator1) 32 | self.stream.writeln() 33 | 34 | # Skip test runner traceback levels 35 | #while tb and self._is_relevant_tb_level(tb): 36 | # tb = tb.tb_next 37 | 38 | self.get_pdb().post_mortem(tb) 39 | 40 | def addFailure(self, test, err): 41 | super(ExceptionTestResultMixin, self).addFailure(test, err) 42 | exctype, value, tb = err 43 | 44 | self.stream.writeln() 45 | self.stream.writeln(self.separator1) 46 | self.stream.writeln(">>> %s" % (self.getDescription(test))) 47 | self.stream.writeln(self.separator2) 48 | self.stream.writeln(self._exc_info_to_string(err, test).rstrip()) 49 | self.stream.writeln(self.separator1) 50 | self.stream.writeln() 51 | 52 | ## Skip test runner traceback levels 53 | #while tb and self._is_relevant_tb_level(tb): 54 | # tb = tb.tb_next 55 | 56 | # Really hacky way to jump up a couple of frames. 57 | # I'm sure it's not that difficult to do properly, 58 | # but I havn't figured out how. 59 | #p = pdb.Pdb() 60 | #p.reset() 61 | #p.setup(None, tb) 62 | #p.do_up(None) 63 | #p.do_up(None) 64 | #p.cmdloop() 65 | 66 | # It would be good if we could make sure we're in the correct frame here 67 | self.get_pdb().post_mortem(tb) 68 | 69 | 70 | class PdbTestResult(ExceptionTestResultMixin, unittest.TextTestResult): 71 | pass 72 | 73 | 74 | class PdbTestRunner(unittest.TextTestRunner): 75 | """ 76 | Override the standard DjangoTestRunner to instead drop into pdb on test errors/failures. 77 | """ 78 | def _makeResult(self): 79 | return PdbTestResult(self.stream, self.descriptions, self.verbosity) 80 | 81 | 82 | class IPdbTestResult(ExceptionTestResultMixin, unittest.TextTestResult): 83 | 84 | pdb_type = 'ipdb' 85 | 86 | 87 | class IPdbTestRunner(unittest.TextTestRunner): 88 | """ 89 | Override the standard DjangoTestRunner to instead drop into ipdb on test errors/failures. 90 | """ 91 | def _makeResult(self): 92 | return IPdbTestResult(self.stream, self.descriptions, self.verbosity) 93 | 94 | 95 | def make_suite_runner(use_ipdb, suite_runner=None): 96 | if use_ipdb: 97 | runner = IPdbTestRunner 98 | else: 99 | runner = PdbTestRunner 100 | 101 | if suite_runner is None: 102 | from django.conf import settings 103 | suite_runner = get_runner(settings) 104 | 105 | class PdbTestSuiteRunner(suite_runner): 106 | """ 107 | Override the standard DjangoTestSuiteRunner to instead drop 108 | into the debugger on test errors/failures. 109 | """ 110 | def run_suite(self, suite, **kwargs): 111 | return runner(verbosity=self.verbosity, 112 | failfast=self.failfast).run(suite) 113 | 114 | return PdbTestSuiteRunner 115 | -------------------------------------------------------------------------------- /testproject/testproject/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for testproject project. 2 | from django import VERSION 3 | 4 | 5 | def ABSOLUTE_PATH(relative_path): 6 | import os 7 | project_path = os.path.abspath(os.path.dirname(__file__)) 8 | return os.path.join(project_path, relative_path) 9 | 10 | # Standard Django settings... 11 | 12 | DEBUG = True 13 | TEMPLATE_DEBUG = DEBUG 14 | TESTING = False 15 | 16 | 17 | if VERSION >= (1, 8): 18 | TEST_RUNNER = 'django.test.runner.DiscoverRunner' 19 | else: 20 | TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' 21 | 22 | ADMINS = ( 23 | # ('Your Name', 'your_email@domain.com'), 24 | ) 25 | 26 | MANAGERS = ADMINS 27 | 28 | DATABASES = { 29 | 'default': { 30 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 31 | 'NAME': ABSOLUTE_PATH('sqlite3.db'), # Or path to database file if using sqlite3. 32 | 'USER': '', # Not used with sqlite3. 33 | 'PASSWORD': '', # Not used with sqlite3. 34 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 35 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 36 | } 37 | } 38 | 39 | # Local time zone for this installation. Choices can be found here: 40 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 41 | # although not all choices may be available on all operating systems. 42 | # On Unix systems, a value of None will cause Django to use the same 43 | # timezone as the operating system. 44 | # If running in a Windows environment this must be set to the same as your 45 | # system time zone. 46 | TIME_ZONE = 'America/Chicago' 47 | 48 | # Language code for this installation. All choices can be found here: 49 | # http://www.i18nguy.com/unicode/language-identifiers.html 50 | LANGUAGE_CODE = 'en-us' 51 | 52 | SITE_ID = 1 53 | 54 | # If you set this to False, Django will make some optimizations so as not 55 | # to load the internationalization machinery. 56 | USE_I18N = True 57 | 58 | # If you set this to False, Django will not format dates, numbers and 59 | # calendars according to the current locale 60 | USE_L10N = True 61 | 62 | # Absolute path to the directory that holds media. 63 | # Example: "/home/media/media.lawrence.com/" 64 | MEDIA_ROOT = '' 65 | 66 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 67 | # trailing slash if there is a path component (optional in other cases). 68 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 69 | MEDIA_URL = '' 70 | 71 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 72 | # trailing slash. 73 | # Examples: "http://foo.com/media/", "/media/". 74 | ADMIN_MEDIA_PREFIX = '/media/' 75 | 76 | # Make this unique, and don't share it with anybody. 77 | SECRET_KEY = '#dg%i9y7=&ptwjv!m1+8lq9l1-27a0s5u85-i-u@-3+1oo2)w-' 78 | 79 | # List of callables that know how to import templates from various sources. 80 | TEMPLATE_LOADERS = ( 81 | 'django.template.loaders.filesystem.Loader', 82 | 'django.template.loaders.app_directories.Loader', 83 | # 'django.template.loaders.eggs.Loader', 84 | ) 85 | 86 | MIDDLEWARE_CLASSES = ( 87 | 'django.middleware.common.CommonMiddleware', 88 | 'django.contrib.sessions.middleware.SessionMiddleware', 89 | 'django.middleware.csrf.CsrfViewMiddleware', 90 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 91 | 'django.contrib.messages.middleware.MessageMiddleware', 92 | 'django_pdb.middleware.PdbMiddleware', 93 | ) 94 | 95 | ROOT_URLCONF = 'testproject.urls' 96 | 97 | TEMPLATE_DIRS = ( 98 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 99 | # Always use forward slashes, even on Windows. 100 | # Don't forget to use absolute paths, not relative paths. 101 | ) 102 | 103 | INSTALLED_APPS = ( 104 | 'django.contrib.auth', 105 | 'django.contrib.contenttypes', 106 | 'django.contrib.sessions', 107 | 'django.contrib.sites', 108 | 'django.contrib.messages', 109 | # Uncomment the next line to enable the admin: 110 | # 'django.contrib.admin', 111 | # Uncomment the next line to enable admin documentation: 112 | # 'django.contrib.admindocs', 113 | 'django_pdb', 114 | 115 | 'testproject.testapp', 116 | ) 117 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Django PDB 2 | ========== 3 | 4 | Make debugging Django easier 5 | ---------------------------- 6 | 7 | Adding ``pdb.set_trace()`` to your source files every time you want to break into pdb sucks. 8 | 9 | Don't do that. 10 | 11 | Do this. 12 | 13 | Installation 14 | ------------ 15 | 16 | Install using pip:: 17 | 18 | pip install django-pdb 19 | 20 | Add it to your settings.py. 21 | 22 | For Django before 1.7 it needs to be added AFTER any apps that override the 23 | `runserver` or `test` commands (includes south and django.contrib.staticfiles). 24 | 25 | For Django after 1.7 it needs to be added BEFORE them. 26 | 27 | .. code:: python 28 | 29 | # Order is important and depends on your Django version. 30 | # With Django 1.7+ put it towards the beginning, otherwise towards the end. 31 | INSTALLED_APPS = ( 32 | ... 33 | 'django_pdb', 34 | ... 35 | ) 36 | 37 | # Make sure to add PdbMiddleware after all other middleware. 38 | # PdbMiddleware only activates when settings.DEBUG is True. 39 | MIDDLEWARE_CLASSES = ( 40 | ... 41 | 'django_pdb.middleware.PdbMiddleware', 42 | ) 43 | 44 | Usage 45 | ----- 46 | 47 | ``manage.py runserver`` 48 | 49 | Drops into pdb at the start of a view if the URL includes a `pdb` GET parameter. 50 | 51 | Drops into ipdb at the start of a view if the URL includes a `ipdb` GET parameter. 52 | 53 | This behavior is only enabled if ``settings.DEBUG = True``:: 54 | 55 | bash: testproject/manage.py runserver 56 | Validating models... 57 | 58 | 0 errors found 59 | Django version 1.3, using settings 'testproject.settings' 60 | Development server is running at http://127.0.0.1:8000/ 61 | Quit the server with CONTROL-C. 62 | 63 | GET /test?pdb 64 | function "myview" in testapp/views.py:7 65 | args: () 66 | kwargs: {} 67 | 68 | > /Users/tom/github/django-pdb/testproject/testapp/views.py(8)myview() 69 | -> a = 1 70 | (Pdb) 71 | 72 | ``manage.py runserver --pdb`` **or** ``manage.py runserver --ipdb`` 73 | 74 | Drops into pdb/ipdb at the start of every view:: 75 | 76 | bash: testproject/manage.py runserver --pdb 77 | Validating models... 78 | 79 | 0 errors found 80 | Django version 1.3, using settings 'testproject.settings' 81 | Development server is running at http://127.0.0.1:8000/ 82 | Quit the server with CONTROL-C. 83 | 84 | GET /test 85 | function "myview" in testapp/views.py:7 86 | args: () 87 | kwargs: {} 88 | 89 | > /Users/tom/github/django-pdb/testproject/testapp/views.py(7)myview() 90 | -> a = 1 91 | (Pdb) 92 | 93 | 94 | ``manage.py test --pdb`` **or** ``manage.py test --ipdb`` 95 | 96 | Drops into pdb/ipdb on test errors/failures:: 97 | 98 | bash: testproject/manage.py test testapp --pdb 99 | Creating test database for alias 'default'... 100 | E 101 | ====================================================================== 102 | >>> test_error (testapp.tests.SimpleTest) 103 | ---------------------------------------------------------------------- 104 | Traceback (most recent call last): 105 | File "/Users/tom/github/django-pdb/testproject/testapp/tests.py", line 16, in test_error 106 | one_plus_one = four 107 | NameError: global name 'four' is not defined 108 | ====================================================================== 109 | 110 | > /Users/tom/github/django-pdb/testproject/testapp/tests.py(16)test_error() 111 | -> one_plus_one = four 112 | (Pdb) 113 | 114 | 115 | Post mortem mode 116 | ---------------- 117 | 118 | ``manage.py runserver --pm`` 119 | 120 | Post mortem mode, drops into (i)pdb if an exception is raised in a view. This works only if there is 121 | no other app overriding ``runserver`` command. 122 | 123 | ``POST_MORTEM = True`` 124 | 125 | You can also add ```POST_MORTEM = True``` to your ```settings.py``` to enable this option even if other app overrides ```runserver```. 126 | 127 | Filter 128 | ------ 129 | 130 | You can also use the template filter ``pdb`` or ``ipdb`` to explore a template variable in (i)pdb this way:: 131 | 132 | {% load pdb %} 133 | 134 | {{ variable|pdb }} 135 | {{ variable|ipdb }} 136 | {{ variable|ipdb|a_filter_to_debug }} 137 | 138 | Example:: 139 | 140 | bash: testproject/manage.py runserver 141 | Validating models... 142 | 143 | 0 errors found 144 | Django version 1.4, using settings 'testproject.settings' 145 | Development server is running at http://127.0.0.1:8000/ 146 | Quit the server with CONTROL-C. 147 | > /Users/tom/github/django-pdb/django_pdb/templatetags/pdb_filters.py(14)pdb() 148 | -> return element 149 | (Pdb) element 150 | "I'm the variable" 151 | (Pdb) element = "another value" 152 | (Pdb) c 153 | [11/May/2012 11:22:53] "GET /filter/ HTTP/1.1" 200 37 154 | 155 | This is useful to inspect a complex object that isn't behaving as expected or debug a filter. 156 | 157 | Other apps that override ``test``/``runserver`` 158 | ----------------------------------------------- 159 | 160 | ``manage.py test --pdb`` works if you also have other apps that 161 | override the ``test`` command, as long as they use Python's unittest 162 | framework. 163 | 164 | Make sure to put ``django_pdb`` **after** any conflicting apps in 165 | ``INSTALLED_APPS`` so that they have priority. 166 | --------------------------------------------------------------------------------