Ticket #5795: startdebuggermode.2.patch

File startdebuggermode.2.patch, 14.0 KB (added by Julian Berman, 9 years ago)
  • twisted/python/failure.py

    diff --git a/twisted/python/failure.py b/twisted/python/failure.py
    index e79862d..90d0afc 100644
    a b import sys 
    1818import linecache
    1919import inspect
    2020import opcode
     21import pdb
    2122from inspect import getmro
    2223
    2324from twisted.python.compat import _PY3, NativeStringIO as StringIO
    def _safeReprVars(varsDictItems): 
    629630# slyphon: make post-morteming exceptions tweakable
    630631
    631632DO_POST_MORTEM = True
     633_Failure__init__ = Failure.__init__
    632634
    633 def _debuginit(self, exc_value=None, exc_type=None, exc_tb=None,
    634                captureVars=False,
    635                Failure__init__=Failure.__init__):
     635
     636def startDebugMode(debugger=None):
    636637    """
    637     Initialize failure object, possibly spawning pdb.
     638    Enable debug hooks for L{Failure}s.
    638639    """
    639     if (exc_value, exc_type, exc_tb) == (None, None, None):
    640         exc = sys.exc_info()
    641         if not exc[0] == self.__class__ and DO_POST_MORTEM:
    642             try:
    643                 strrepr = str(exc[1])
    644             except:
    645                 strrepr = "broken str"
    646             print("Jumping into debugger for post-mortem of exception '%s':" % (strrepr,))
    647             import pdb
    648             pdb.post_mortem(exc[2])
    649     Failure__init__(self, exc_value, exc_type, exc_tb, captureVars)
    650 
    651 
    652 def startDebugMode():
    653     """Enable debug hooks for Failures."""
     640
     641    if debugger is None:
     642        debugger = pdb
     643    post_mortem = getattr(debugger, "post_mortem", pdb.post_mortem)
     644
     645    def _debuginit(self, exc_value=None, exc_type=None, exc_tb=None,
     646                   captureVars=False):
     647        """
     648        Initialize failure object, possibly spawning the debugger.
     649        """
     650        if (exc_value, exc_type, exc_tb) == (None, None, None):
     651            exc = sys.exc_info()
     652            if not exc[0] == self.__class__ and DO_POST_MORTEM:
     653                try:
     654                    strrepr = str(exc[1])
     655                except:
     656                    strrepr = "broken str"
     657                print("Jumping into debugger for post-mortem of exception '%s':" % (strrepr,))
     658                post_mortem(exc[2])
     659        _Failure__init__(self, exc_value, exc_type, exc_tb, captureVars)
     660
    654661    Failure.__init__ = _debuginit
     662
     663
     664def stopDebugMode():
     665    """
     666    Disable debug hooks for L{Failure}s.
     667    """
     668    Failure.__init__ = _Failure__init__
  • twisted/scripts/trial.py

    diff --git a/twisted/scripts/trial.py b/twisted/scripts/trial.py
    index 46859d5..5ffed53 100644
    a b def _reporterAction(): 
    9393                               plugin.getPlugins(itrial.IReporter)])
    9494
    9595
     96def _wrappedPdb():
     97    """
     98    Wrap an instance of C{pdb.Pdb} with readline support and load any .rcs.
     99
     100    """
     101
     102    dbg = pdb.Pdb()
     103    try:
     104        import readline
     105    except ImportError:
     106        print "readline module not available"
     107        sys.exc_clear()
     108    for path in ('.pdbrc', 'pdbrc'):
     109        if os.path.exists(path):
     110            try:
     111                rcFile = file(path, 'r')
     112            except IOError:
     113                sys.exc_clear()
     114            else:
     115                dbg.rcLines.extend(rcFile.readlines())
     116    return dbg
     117
    96118
    97119class _BasicOptions(object):
    98120    """
    class _BasicOptions(object): 
    207229        sys.exit(0)
    208230
    209231
     232    def opt_debugger(self, debugger):
     233        """
     234        Load a debugger.
     235        """
     236        if debugger == 'pdb':
     237            self["debugger"] = _wrappedPdb()
     238            return
     239
     240        try:
     241            self["debugger"] = reflect.namedAny(debugger)
     242        except reflect.ModuleNotFound:
     243            raise usage.UsageError(
     244                "%r debugger could not be found." % (debugger,))
     245
     246
    210247    def opt_disablegc(self):
    211248        """
    212249        Disable the garbage collector
    class Options(_BasicOptions, usage.Options, app.ReactorSelectionMixin): 
    321358                ]
    322359
    323360    optParameters = [
    324         ["debugger", None, "pdb", "the fully qualified name of a debugger to "
    325          "use if --debug is passed"],
     361        ["debugger", None, _wrappedPdb(),
     362         "the fully qualified name of a debugger to use if --debug is passed"],
    326363        ["logfile", "l", "test.log", "log file name"],
    327364        ["jobs", "j", None, "Number of local workers to run"]
    328365        ]
    class Options(_BasicOptions, usage.Options, app.ReactorSelectionMixin): 
    390427def _initialDebugSetup(config):
    391428    # do this part of debug setup first for easy debugging of import failures
    392429    if config['debug']:
    393         failure.startDebugMode()
     430        failure.startDebugMode(debugger=config["debugger"])
    394431    if config['debug'] or config['debug-stacktraces']:
    395432        defer.setDebugging(True)
    396433
    def _getLoader(config): 
    415452    return loader
    416453
    417454
    418 def _wrappedPdb():
    419     """
    420     Wrap an instance of C{pdb.Pdb} with readline support and load any .rcs.
    421 
    422     """
    423 
    424     dbg = pdb.Pdb()
    425     try:
    426         import readline
    427     except ImportError:
    428         print "readline module not available"
    429         sys.exc_clear()
    430     for path in ('.pdbrc', 'pdbrc'):
    431         if os.path.exists(path):
    432             try:
    433                 rcFile = file(path, 'r')
    434             except IOError:
    435                 sys.exc_clear()
    436             else:
    437                 dbg.rcLines.extend(rcFile.readlines())
    438     return dbg
    439 
    440 
    441 class _DebuggerNotFound(Exception):
    442     """
    443     A debugger import failed.
    444 
    445     Used to allow translating these errors into usage error messages.
    446 
    447     """
    448 
    449 
    450 
    451455def _makeRunner(config):
    452456    """
    453457    Return a trial runner class set up with the parameters extracted from
    def _makeRunner(config): 
    474478    else:
    475479        if config['debug']:
    476480            args['mode'] = runner.TrialRunner.DEBUG
    477             debugger = config['debugger']
    478 
    479             if debugger != 'pdb':
    480                 try:
    481                     args['debugger'] = reflect.namedAny(debugger)
    482                 except reflect.ModuleNotFound:
    483                     raise _DebuggerNotFound(
    484                         '%r debugger could not be found.' % (debugger,))
    485             else:
    486                 args['debugger'] = _wrappedPdb()
    487 
     481            args['debugger'] = config['debugger']
    488482        args['profile'] = config['profile']
    489483        args['forceGarbageCollection'] = config['force-gc']
    490484
    def run(): 
    500494        config.parseOptions()
    501495    except usage.error, ue:
    502496        raise SystemExit, "%s: %s" % (sys.argv[0], ue)
    503     _initialDebugSetup(config)
    504 
    505     try:
    506         trialRunner = _makeRunner(config)
    507     except _DebuggerNotFound as e:
    508         raise SystemExit('%s: %s' % (sys.argv[0], str(e)))
    509497
     498    _initialDebugSetup(config)
     499    trialRunner = _makeRunner(config)
    510500    suite = _getSuite(config)
     501
    511502    if config['until-failure']:
    512503        test_result = trialRunner.runUntilFailure(suite)
    513504    else:
  • twisted/test/test_failure.py

    diff --git a/twisted/test/test_failure.py b/twisted/test/test_failure.py
    index cbcda41..c7aece7 100644
    a b class TestFrameAttributes(SynchronousTestCase): 
    752752
    753753
    754754
     755class StubDebugger(object):
     756    """
     757    Used to test L{Failure.startDebugMode}.
     758    """
     759    def __init__(self):
     760        self.mortems = []
     761        self.post_mortem = self.mortems.append
     762
     763
     764
    755765class TestDebugMode(SynchronousTestCase):
    756766    """
    757     Failure's debug mode should allow jumping into the debugger.
     767    Failure's debug mode should allow jumping into a debugger.
    758768    """
    759769
    760770    def setUp(self):
    761         """
    762         Override pdb.post_mortem so we can make sure it's called.
    763         """
    764         # Make sure any changes we make are reversed:
    765         post_mortem = pdb.post_mortem
    766         if _PY3:
    767             origInit = failure.Failure.__init__
    768         else:
    769             origInit = failure.Failure.__dict__['__init__']
    770         def restore():
    771             pdb.post_mortem = post_mortem
    772             if _PY3:
    773                 failure.Failure.__init__ = origInit
    774             else:
    775                 failure.Failure.__dict__['__init__'] = origInit
    776         self.addCleanup(restore)
    777 
    778         self.result = []
    779         pdb.post_mortem = self.result.append
    780         failure.startDebugMode()
     771        self.debugger = StubDebugger()
    781772
     773    def tearDown(self):
     774        failure.stopDebugMode()
    782775
    783776    def test_regularFailure(self):
    784777        """
    785         If startDebugMode() is called, calling Failure() will first call
    786         pdb.post_mortem with the traceback.
     778        If L{Failure.startDebugMode} is called, instantiating L{Failure} will
     779        first call the debugger's C{post_mortem} method with the traceback.
    787780        """
     781        failure.startDebugMode(self.debugger)
     782
    788783        try:
    789             1/0
     784            raise ZeroDivisionError()
    790785        except:
    791             typ, exc, tb = sys.exc_info()
     786            _, _, tb = sys.exc_info()
    792787            f = failure.Failure()
    793         self.assertEqual(self.result, [tb])
    794         self.assertEqual(f.captureVars, False)
     788        self.assertEqual(self.debugger.mortems, [tb])
     789        self.assertFalse(f.captureVars)
    795790
    796791
    797792    def test_captureVars(self):
    798793        """
    799         If startDebugMode() is called, passing captureVars to Failure() will
    800         not blow up.
     794        If L{Failure.startDebugMode} is called, instantiating L{Failure} with
     795        C{captureVars} still works.
    801796        """
     797        failure.startDebugMode(self.debugger)
     798
    802799        try:
    803             1/0
     800            raise ZeroDivisionError()
    804801        except:
    805             typ, exc, tb = sys.exc_info()
     802            _, _, tb = sys.exc_info()
    806803            f = failure.Failure(captureVars=True)
    807         self.assertEqual(self.result, [tb])
    808         self.assertEqual(f.captureVars, True)
     804        self.assertEqual(self.debugger.mortems, [tb])
     805        self.assertTrue(f.captureVars)
     806
     807
     808    def test_defaultDebuggerIsPDB(self):
     809        """
     810        If L{Failure.startDebugMode} is called without a debugger, L{pdb} is
     811        used.
     812        """
     813        self.patch(pdb, "post_mortem", self.debugger.post_mortem)
     814        failure.startDebugMode()
     815
     816        try:
     817            raise ZeroDivisionError()
     818        except:
     819            _, _, tb = sys.exc_info()
     820            f = failure.Failure()
     821        self.assertEqual(self.debugger.mortems, [tb])
     822
     823
     824    def test_missingPostMortem(self):
     825        """
     826        If L{Failure.startDebugMode} is called with a debugger without a
     827        C{post_mortem}, method L{pdb} is used.
     828        """
     829        self.patch(pdb, "post_mortem", self.debugger.post_mortem)
     830        failure.startDebugMode(object())
     831
     832        try:
     833            raise ZeroDivisionError()
     834        except:
     835            _, _, tb = sys.exc_info()
     836            f = failure.Failure()
     837        self.assertEqual(self.debugger.mortems, [tb])
    809838
    810839
    811840
  • new file twisted/topfiles/5795.feature

    diff --git a/twisted/topfiles/5795.feature b/twisted/topfiles/5795.feature
    new file mode 100644
    index 0000000..7c99f32
    - +  
     1failure.startDebugMode now takes a debugger to drop into, and respects --debugger as provided on the command line.
  • twisted/trial/test/test_script.py

    diff --git a/twisted/trial/test/test_script.py b/twisted/trial/test/test_script.py
    index 45a254b..e8280a5 100644
    a b from twisted.trial.runner import ( 
    99    TrialRunner, TestSuite, DestructiveTestSuite, TestLoader)
    1010from twisted.trial._dist.disttrial import DistTrialRunner
    1111from twisted.scripts import trial
    12 from twisted.python import util
     12from twisted.python import failure, util
    1313from twisted.python.usage import UsageError
    1414from twisted.python.filepath import FilePath
    1515
    class MakeRunnerTestCase(unittest.TestCase): 
    551551
    552552
    553553    def test_DebuggerNotFound(self):
     554        """
     555        If a debugger cannot be found using L{namedAny}, a L{UsageError} is
     556        raised.
     557        """
    554558        namedAny = trial.reflect.namedAny
    555559
    556560        def namedAnyExceptdoNotFind(fqn):
    class MakeRunnerTestCase(unittest.TestCase): 
    561565        self.patch(trial.reflect, "namedAny", namedAnyExceptdoNotFind)
    562566
    563567        options = trial.Options()
    564         options.parseOptions(["--debug", "--debugger", "doNotFind"])
    565 
    566         self.assertRaises(trial._DebuggerNotFound, trial._makeRunner, options)
     568        self.assertRaises(
     569            UsageError,
     570            options.parseOptions,
     571            ["--debug", "--debugger", "doNotFind"])
    567572
    568573
    569574class TestRun(unittest.TestCase):
    class TestRun(unittest.TestCase): 
    572577    """
    573578
    574579    def setUp(self):
    575         # don't re-parse cmdline options, because if --reactor was passed to
    576         # the test run trial will try to restart the (already running) reactor
    577         self.patch(trial.Options, "parseOptions", lambda self: None)
     580        # Calling trial.run() will call usage.Options().parseOptions(), which
     581        # grabs sys.argv. Patch it, so it doesn't get the arguments from the
     582        # outer trial run.
     583        self.argv = [sys.argv[0]]
     584        self.patch(trial.sys, "argv", self.argv)
     585
     586
     587    def test_setsUpFailureDebugMode(self):
     588        """
     589        When a debug mode is enabled, L{failure.startDebugMode} is called with
     590        the provided debugger.
     591
     592        """
     593
     594        self.argv.extend(
     595            [
     596                "--debug",
     597                "--debugger",
     598                "twisted.trial.test.test_runner.TestRunner.cdebugger",
     599            ],
     600        )
     601
     602        def recordDebugger(debugger):
     603            self.debugger = debugger
     604        self.patch(failure, "startDebugMode", recordDebugger)
     605
     606        try:
     607            trial.run()
     608        except SystemExit:
     609            pass
     610
     611        from twisted.trial.test.test_runner import TestRunner
     612        self.assertEqual(self.debugger, TestRunner.cdebugger)
    578613
    579614
    580615    def test_debuggerNotFound(self):
    class TestRun(unittest.TestCase): 
    583618
    584619        """
    585620
    586         def _makeRunner(*args, **kwargs):
    587             raise trial._DebuggerNotFound('foo')
    588         self.patch(trial, "_makeRunner", _makeRunner)
     621        self.argv.extend(["--debug", "--debugger", "foo"])
     622        self.addCleanup(trial.failure.stopDebugMode)
    589623
    590624        try:
    591625            trial.run()