dispatch based on frame
authorMark Florisson <markflorisson88@gmail.com>
Sun, 31 Oct 2010 21:34:38 +0000 (22:34 +0100)
committerMark Florisson <markflorisson88@gmail.com>
Sun, 31 Oct 2010 21:34:38 +0000 (22:34 +0100)
python code stepping (for libpython and libcython)
generic stepper class
fix step-into functions
have cygdb accept a '--' command line argument to disable automatic importing
replace gdb.execute() with something that actually captures all output
have 'cy break' break properly on line numbers

Cython/Compiler/Main.py
Cython/Compiler/ParseTreeTransforms.py
Cython/Debugger/Cygdb.py
Cython/Debugger/DebugWriter.py [new file with mode: 0644]
Cython/Debugger/libcython.py
Cython/Debugger/libpython.py
bin/cygdb
cygdb.py

index 6275fb12538f32aac8666597cd89a74c302ebc2b..75a80414af859c39c06332999d63f2caddabfe7a 100644 (file)
@@ -185,7 +185,7 @@ class Context(object):
             from ParseTreeTransforms import DebugTransform
             self.debug_outputwriter = DebugWriter.CythonDebugWriter(
                 options.output_dir)
-            debug_transform = [DebugTransform(self)]
+            debug_transform = [DebugTransform(self, options)]
         else:
             debug_transform = []
             
index cfc9f90c6ffb157d94327b5503317185aff8149f..5b34f8d6c6264c4a2c84a7a5091cfb20ec1153ef 100644 (file)
@@ -1441,18 +1441,23 @@ class DebugTransform(CythonTransform):
     to enable debugging.
     """
     
-    def __init__(self, context):
+    def __init__(self, context, options):
         super(DebugTransform, self).__init__(context)
         self.visited = set()
         # our treebuilder and debug output writer 
         # (see Cython.Debugger.debug_output.CythonDebugWriter)
         self.tb = self.context.debug_outputwriter
+        self.c_output_file = options.output_file
+        
+        # tells visit_NameNode whether it should register step-into functions
+        self.register_stepinto = False
         
     def visit_ModuleNode(self, node):
         self.tb.module_name = node.full_module_name
         attrs = dict(
             module_name=node.full_module_name,
-            filename=node.pos[0].filename)
+            filename=node.pos[0].filename,
+            c_filename=self.c_output_file)
         
         self.tb.start('Module', attrs)
         
@@ -1503,20 +1508,25 @@ class DebugTransform(CythonTransform):
         self.tb.end('Arguments')
 
         self.tb.start('StepIntoFunctions')
+        self.register_stepinto = True
         self.visitchildren(node)
+        self.register_stepinto = False
         self.tb.end('StepIntoFunctions')
         self.tb.end('Function')
 
         return node
 
     def visit_NameNode(self, node):
-        if (node.type.is_cfunction and 
-            node.is_called and 
-            node.entry.in_cinclude):
+        if (self.register_stepinto and node.type.is_cfunction and node.is_called):
+            # don't check node.entry.in_cinclude, as 'cdef extern: ...' 
+            # declared functions are not 'in_cinclude'. 
+            # This means we will list called 'cdef' functions as 
+            # "step into functions", but this is not an issue as they will be 
+            # recognized as Cython functions anyway.
             attrs = dict(name=node.entry.func_cname)
             self.tb.start('StepIntoFunction', attrs=attrs)
             self.tb.end('StepIntoFunction')
-            
+        
         self.visitchildren(node)
         return node
     
index 67f5be91b75a1fbae2af1622c6c3cd803439813b..6045659b4781ed8ac2523cb097893148b1bd5055 100644 (file)
@@ -20,25 +20,29 @@ import subprocess
 def usage():
     print("Usage: cygdb [PATH GDB_ARGUMENTS]")
 
-def make_command_file(path_to_debug_info):
-    debug_files = glob.glob(
-        os.path.join(path_to_debug_info, 'cython_debug/cython_debug_info_*'))
-
-    if not debug_files:
-        usage()
-        sys.exit('No debug files were found in %s. Aborting.' % (
-                 os.path.abspath(path_to_debug_info)))
+def make_command_file(path_to_debug_info, prefix_code='', no_import=False):
+    if not no_import:
+        pattern = os.path.join(path_to_debug_info, 
+                               'cython_debug/cython_debug_info_*')
+        debug_files = glob.glob(pattern)
+
+        if not debug_files:
+            usage()
+            sys.exit('No debug files were found in %s. Aborting.' % (
+                    os.path.abspath(path_to_debug_info)))
         
     fd, tempfilename = tempfile.mkstemp()
     f = os.fdopen(fd, 'w')
+    f.write(prefix_code)
     f.write('set breakpoint pending on\n')
     f.write('python from Cython.Debugger import libcython\n')
-    f.write('\n'.join('cy import %s\n' % fn for fn in debug_files))
+    if not no_import:
+        f.write('\n'.join('cy import %s\n' % fn for fn in debug_files))
     f.close()
     
     return tempfilename
 
-def main(gdb_argv=[], path_to_debug_info=os.curdir):
+def main(path_to_debug_info=os.curdir, gdb_argv=[], no_import=False):
     """
     Start the Cython debugger. This tells gdb to import the Cython and Python
     extensions (libpython.py and libcython.py) and it enables gdb's pending 
@@ -46,7 +50,7 @@ def main(gdb_argv=[], path_to_debug_info=os.curdir):
     
     path_to_debug_info is the path to the cython_debug directory
     """
-    tempfilename = make_command_file(path_to_debug_info)
+    tempfilename = make_command_file(path_to_debug_info, no_import=no_import)
     p = subprocess.Popen(['gdb', '-command', tempfilename] + gdb_argv)
     while True:
         try:
diff --git a/Cython/Debugger/DebugWriter.py b/Cython/Debugger/DebugWriter.py
new file mode 100644 (file)
index 0000000..6ec0fab
--- /dev/null
@@ -0,0 +1,71 @@
+import os
+import errno
+
+try:
+  from lxml import etree
+  have_lxml = True
+except ImportError:
+    have_lxml = False
+    try:
+        # Python 2.5
+        from xml.etree import cElementTree as etree
+    except ImportError:
+        try:
+            # Python 2.5
+            from xml.etree import ElementTree as etree
+        except ImportError:
+            try:
+                # normal cElementTree install
+                import cElementTree as etree
+            except ImportError:
+                try:
+                    # normal ElementTree install
+                    import elementtree.ElementTree as etree
+                except ImportError:
+                    etree = None
+
+from Cython.Compiler import Errors
+
+
+class CythonDebugWriter(object):
+    """
+    Class to output debugging information for cygdb
+    
+    It writes debug information to cython_debug/cython_debug_info_<modulename>
+    in the build directory.
+    """
+    
+    def __init__(self, output_dir):
+        if etree is None:
+            raise Errors.NoElementTreeInstalledException()
+        
+        self.output_dir = os.path.join(output_dir, 'cython_debug')
+        self.tb = etree.TreeBuilder()
+        # set by Cython.Compiler.ParseTreeTransforms.DebugTransform
+        self.module_name = None
+        self.start('cython_debug', attrs=dict(version='1.0'))
+    
+    def start(self, name, attrs=None):
+        self.tb.start(name, attrs or {})
+    
+    def end(self, name):
+        self.tb.end(name)
+
+    def serialize(self):
+        self.tb.end('Module')
+        self.tb.end('cython_debug')
+        xml_root_element = self.tb.close()
+
+        try:
+            os.makedirs(self.output_dir)
+        except OSError, e:
+            if e.errno != errno.EEXIST:
+                raise
+
+        et = etree.ElementTree(xml_root_element)
+        kw = {}
+        if have_lxml:
+            kw['pretty_print'] = True
+        
+        fn = "cython_debug_info_" + self.module_name
+        et.write(os.path.join(self.output_dir, fn), encoding="UTF-8", **kw)
\ No newline at end of file
index b0aa69f602d7bae6e08eab7cc738466b425dffbf..c6cca5a41c75ad8e68253195d5eb01ff8598a723 100644 (file)
@@ -90,20 +90,43 @@ def require_cython_frame(function):
                                'Cython function we know about.')
         return function(self, *args, **kwargs)
     return wrapper 
+
+def dispatch_on_frame(c_command, python_command=None):
+    def decorator(function):
+        @functools.wraps(function)
+        def wrapper(self, *args, **kwargs):
+            is_cy = self.is_cython_function()
+            is_py = self.is_python_function()
+            
+            if is_cy or (is_py and not python_command):
+                function(self, *args, **kwargs)
+            elif is_py:
+                gdb.execute(python_command)
+            elif self.is_relevant_function():
+                gdb.execute(c_command)
+            else:
+                raise gdb.GdbError("Not a function cygdb knows about. "
+                                   "Use the normal GDB commands instead.")
         
+        return wrapper
+    return decorator
+
+
 # Classes that represent the debug information
 # Don't rename the parameters of these classes, they come directly from the XML
 
 class CythonModule(object):
-    def __init__(self, module_name, filename):
+    def __init__(self, module_name, filename, c_filename):
         self.name = module_name
         self.filename = filename
+        self.c_filename = c_filename
         self.globals = {}
         # {cython_lineno: min(c_linenos)}
         self.lineno_cy2c = {}
         # {c_lineno: cython_lineno}
         self.lineno_c2cy = {}
-    
+        self.functions = {}
+        
     def qualified_name(self, varname):
         return '.'.join(self.name, varname)
 
@@ -146,12 +169,15 @@ class CythonBase(object):
 
     @default_selected_gdb_frame(err=False)
     def is_python_function(self, frame):
-        return frame.name() == 'PyEval_EvalFrameEx'
-
-    @default_selected_gdb_frame()
-    def is_python_function(self, frame):
-        return libpython.Frame(frame).is_evalframeex()
-
+        """
+        Tells if a frame is associated with a Python function.
+        If we can't read the Python frame information, don't regard it as such.
+        """
+        if frame.name() == 'PyEval_EvalFrameEx':
+            pyframe = libpython.Frame(frame).get_pyop()
+            return pyframe and not pyframe.is_optimized_out()
+        return False
+        
     @default_selected_gdb_frame()
     def get_c_function_name(self, frame):
         return frame.name()
@@ -170,9 +196,13 @@ class CythonBase(object):
     
     @default_selected_gdb_frame()
     def get_cython_lineno(self, frame):
+        """
+        Get the current Cython line number. Returns 0 if there is no 
+        correspondence between the C and Cython code.
+        """
         cyfunc = self.get_cython_function(frame)
-        return cyfunc.module.lineno_c2cy.get(self.get_c_lineno(frame))
-
+        return cyfunc.module.lineno_c2cy.get(self.get_c_lineno(frame), 0)
+    
     @default_selected_gdb_frame()
     def get_source_desc(self, frame):
         filename = lineno = lexer = None
@@ -185,7 +215,7 @@ class CythonBase(object):
             pyframeobject = libpython.Frame(frame).get_pyop()
 
             if not pyframeobject:
-                raise GdbError('Unable to read information on python frame')
+                raise gdb.GdbError('Unable to read information on python frame')
 
             filename = pyframeobject.filename()
             lineno = pyframeobject.current_line_num()
@@ -198,6 +228,25 @@ class CythonBase(object):
     def get_source_line(self, frame):
         source_desc, lineno = self.get_source_desc()
         return source_desc.get_source(lineno)
+    
+    def is_relevant_function(self, frame):
+        """
+        returns whether we care about a frame on the user-level when debugging
+        Cython code
+        """
+        name = frame.name()
+        older_frame = frame.older()
+        
+        if self.is_cython_function(frame) or self.is_python_function(frame):
+            return True
+        elif (parameters.step_into_c_code and 
+              older_frame and self.is_cython_function(older_frame)):
+            # direct C function call from a Cython function
+            cython_func = self.get_cython_function(older_frame)
+            return name in cython_func.step_into_functions
+
+        return False
+
 
 class SourceFileDescriptor(object):
     def __init__(self, filename, lexer, formatter=None):
@@ -238,13 +287,20 @@ class SourceFileDescriptor(object):
                 yield '%s %4d    %s' % (prefix, start + idx, line)
 
     def get_source(self, start, stop=None, lex_source=True, mark_line=0):
+        exc = gdb.GdbError('Unable to retrieve source code')
+        
         if not self.filename:
-            raise GdbError('Unable to retrieve source code')
-
+            raise exc
+            
         if stop is None:
             stop = start + 1
-        return '\n'.join(self._get_source(start, stop, lex_source, mark_line))
-        
+
+        try:
+            return '\n'.join(
+                self._get_source(start, stop, lex_source, mark_line))
+        except IOError:
+            raise exc
+
 
 # Errors
 
@@ -299,18 +355,24 @@ class CompleteUnqualifiedFunctionNames(CythonParameter):
 
 class ColorizeSourceCode(CythonParameter):
     """
-    Tell cygdb whether to colorize source code
+    Tell cygdb whether to colorize source code.
     """
 
 class TerminalBackground(CythonParameter):
     """
-    Tell cygdb about the user's terminal background (light or dark)
+    Tell cygdb about the user's terminal background (light or dark).
     """
-    
+
+class StepIntoCCode(CythonParameter):
+    """
+    Tells cygdb whether to step into C functions called directly from Cython
+    code.
+    """
+
 class CythonParameters(object):
     """
     Simple container class that might get more functionality in the distant
-    future (mostly to remind us that we're dealing with parameters)
+    future (mostly to remind us that we're dealing with parameters).
     """
     
     def __init__(self):
@@ -329,7 +391,12 @@ class CythonParameters(object):
             gdb.COMMAND_FILES,
             gdb.PARAM_STRING,
             "dark")
-
+        self.step_into_c_code = StepIntoCCode(
+            'cy_step_into_c_code',
+            gdb.COMMAND_RUNNING,
+            gdb.PARAM_BOOLEAN,
+            True)
+        
 parameters = CythonParameters()
 
 
@@ -340,6 +407,14 @@ class CythonCommand(gdb.Command, CythonBase):
     Base class for Cython commands
     """
 
+    @classmethod
+    def register(cls, *args, **kwargs):
+        if not hasattr(cls, 'completer_class'):
+            return cls(cls.name, cls.command_class, *args, **kwargs)
+        else:
+            return cls(cls.name, cls.command_class, cls.completer_class, 
+                       *args, **kwargs)
+
 
 class CyCy(CythonCommand):
     """
@@ -348,6 +423,7 @@ class CyCy(CythonCommand):
         cy import
         cy break
         cy step
+        cy next
         cy print
         cy list
         cy locals
@@ -357,31 +433,29 @@ class CyCy(CythonCommand):
         cy down
     """
     
-    def __init__(self):
-        super(CythonCommand, self).__init__(
-            'cy', gdb.COMMAND_NONE, gdb.COMPLETE_COMMAND, prefix=True)
-        
-        self.import_ = CyImport(
-            'cy import', gdb.COMMAND_STATUS, gdb.COMPLETE_FILENAME)
-            
-        self.break_ = CyBreak('cy break', gdb.COMMAND_BREAKPOINTS)
-        self.step = CyStep('cy step', gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE)
-        self.next = CyNext('cy next', gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE)
-        self.list = CyList('cy list', gdb.COMMAND_FILES, gdb.COMPLETE_NONE)
-        self.print_ = CyPrint('cy print', gdb.COMMAND_DATA)
+    name = 'cy'
+    command_class = gdb.COMMAND_NONE
+    completer_class = gdb.COMPLETE_COMMAND
+    
+    def __init__(self, *args):
+        super(CythonCommand, self).__init__(*args, prefix=True)
         
-        self.locals = CyLocals(
-            'cy locals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE)
-        self.globals = CyGlobals(
-            'cy globals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE)
+        commands = dict(
+            import_ = CyImport.register(),
+            break_ = CyBreak.register(),
+            step = CyStep.register(),
+            next = CyNext.register(),
+            list = CyList.register(),
+            print_ = CyPrint.register(),
+            locals = CyLocals.register(),
+            globals = CyGlobals.register(),
+            cy_cname = CyCName('cy_cname'),
+            cy_line = CyLine('cy_line'),
+        )
             
-        self.cy_cname = CyCName('cy_cname')
-        
-        objs = (self.import_, self.break_, self.step, self.list, self.print_,
-                self.locals, self.globals, self.cy_cname)
-
-        for obj in objs:
-            obj.cy = self
+        for command_name, command in commands.iteritems():
+            command.cy = self
+            setattr(self, command_name, command)
             
         # Cython module namespace
         self.cython_namespace = {}
@@ -403,15 +477,19 @@ class CyImport(CythonCommand):
     Import debug information outputted by the Cython compiler
     Example: cy import FILE...
     """
-
+    
+    name = 'cy import'
+    command_class = gdb.COMMAND_STATUS
+    completer_class = gdb.COMPLETE_FILENAME
+    
     def invoke(self, args, from_tty):
         args = args.encode(_filesystemencoding)
         for arg in string_to_argv(args):
             try:
                 f = open(arg)
             except OSError, e:
-                print('Unable to open file %r: %s' % (args, e.args[1]))
-                return
+                raise gdb.GdbError('Unable to open file %r: %s' % 
+                                                (args, e.args[1]))
             
             t = etree.parse(f)
             
@@ -426,15 +504,20 @@ class CyImport(CythonCommand):
                 for function in module.find('Functions'):
                     cython_function = CythonFunction(module=cython_module, 
                                                      **function.attrib)
-                    
+
                     # update the global function mappings
-                    self.cy.functions_by_name[cython_function.name].append(
-                        cython_function)
+                    name = cython_function.name
+                    
+                    self.cy.functions_by_name[name].append(cython_function)
                     self.cy.functions_by_qualified_name[
                         cython_function.qualified_name] = cython_function
                     self.cy.functions_by_cname[
                         cython_function.cname] = cython_function
                     
+                    d = cython_module.functions
+                    L = d.setdefault(cython_function.qualified_name, [])
+                    L.append(cython_function)
+                    
                     for local in function.find('Locals'):
                         d = local.attrib
                         cython_function.locals[d['name']] = CythonVariable(**d)
@@ -469,13 +552,16 @@ class CyBreak(CythonCommand):
         cy break cython_module:lineno...
     """
     
+    name = 'cy break'
+    command_class = gdb.COMMAND_BREAKPOINTS
+    
     def _break_pyx(self, name):
         modulename, _, lineno = name.partition(':')
         lineno = int(lineno)
         cython_module = self.cy.cython_namespace[modulename]
         if lineno in cython_module.lineno_cy2c:
             c_lineno = cython_module.lineno_cy2c[lineno]
-            breakpoint = '%s:%s' % (cython_module.name, c_lineno)
+            breakpoint = '%s:%s' % (cython_module.c_filename, c_lineno)
             gdb.execute('break ' + breakpoint)
         else:
             raise GdbError("Not a valid line number. "
@@ -554,78 +640,74 @@ class CyBreak(CythonCommand):
         return compl
 
 
-class CodeStepperMixin(object):
+class CythonCodeStepper(CythonCommand, libpython.GenericCodeStepper):
+    """
+    Base class for CyStep and CyNext. It implements the interface dictated by
+    libpython.GenericCodeStepper.
+    """
     
-    def init_stepping(self):
-        self.cython_func = self.get_cython_function()
-        self.beginline = self.get_cython_lineno()
-        self.curframe = gdb.selected_frame() 
-
-    def next_step(self, command):
-        "returns whether to continue stepping"
-        result = gdb.execute(command, to_string=True)
-        newframe = gdb.selected_frame()
+    def lineno(self, frame):
+        # Take care of the Python and Cython levels. We need to care for both
+        # as we can't simply dispath to 'py-step', since that would work for
+        # stepping through Python code, but it would not step back into Cython-
+        # related code. The C level should be dispatched to the 'step' command.
+        if self.is_cython_function(frame):
+            return self.get_cython_lineno(frame)
+        else:
+            return libpython.py_step.lineno(frame)
+    
+    def get_source_line(self, frame):
+        # We may have ended up in a Python, Cython, or C function
+        # In case of C, don't display any additional data (gdb already 
+        # does this)
+        result = ''
+        
+        if self.is_cython_function(frame) or self.is_python_function(frame):
+            try:
+                result = super(CythonCodeStepper, self).get_source_line(frame)
+            except gdb.GdbError:
+                result = ''
         
-        c1 = result.startswith('Breakpoint')
-        c2 = (newframe == self.curframe and 
-              self.get_cython_lineno() > self.beginline)
-        return not c1 and not c2
+        return result.lstrip()
         
-    def end_stepping(self):
-        sys.stdout.write(self.get_source_line())
+    @classmethod
+    def register(cls):
+        return cls(cls.name, stepper=cls.stepper)
+    
 
+class CyStep(CythonCodeStepper):
+    "Step through Python code."
+    
+    name = 'cy step'
+    stepper = True
+    
+    @dispatch_on_frame(c_command='step')
+    def invoke(self, *args, **kwargs):
+        super(CythonCodeStepper, self).invoke(*args, **kwargs)
 
-class CyStep(CythonCommand, CodeStepperMixin):
 
-    def step(self, nsteps=1):
-        for nthstep in xrange(nsteps):
-            self.init_stepping()
-            
-            while self.next_step('step'):
-                newframe = gdb.selected_frame()
-                if newframe != self.curframe:
-                    # we entered a function
-                    funcname = self.get_c_function_name(newframe)
-                    if (self.is_cython_function() or 
-                        self.is_python_function() or
-                        funcname in cython_function.step_into_functions):
-                        break
-        
-        self.end_stepping()
-
-    def invoke(self, steps, from_tty):
-        if self.is_cython_function():
-            if steps:
-                self.step(int(steps))
-            else:
-                self.step()
-        else:
-            gdb.execute('step ' + steps, from_tty)
+class CyNext(CythonCodeStepper):
+    "Step-over Python code."
 
+    name = 'cy next'
+    stepper = False
 
-class CyNext(CythonCommand, CodeStepperMixin):
-    
-    def next(self, nsteps=1):
-        for nthstep in xrange(nsteps):
-            self.init_stepping()
-            
-            while self.next_step('next'):
-                pass
-            
-            self.end_stepping()
-    
-    def invoke(self, steps, from_tty):
-        if self.is_cython_function():
-            if steps:
-                self.next(int(steps))
-            else:
-                self.next()
-        else:
-            gdb.execute('next ' + steps, from_tty)
+    @dispatch_on_frame(c_command='next')
+    def invoke(self, *args, **kwargs):
+        super(CythonCodeStepper, self).invoke(*args, **kwargs)
 
 
 class CyList(CythonCommand):
+    """
+    List Cython source code. To disable to customize colouring see the cy_*
+    parameters.
+    """
     
+    name = 'cy list'
+    command_class = gdb.COMMAND_FILES
+    completer_class = gdb.COMPLETE_NONE
+    
+    @dispatch_on_frame(c_command='list')
     def invoke(self, _, from_tty):
         sd, lineno = self.get_source_desc()
         source = sd.get_source(lineno - 5, lineno + 5, mark_line=lineno)
@@ -637,27 +719,34 @@ class CyPrint(CythonCommand):
     Print a Cython variable using 'cy-print x' or 'cy-print module.function.x'
     """
     
+    name = 'cy print'
+    command_class = gdb.COMMAND_DATA
+    
+    @dispatch_on_frame(c_command='print', python_command='py-print')
     def invoke(self, name, from_tty):
-        try:
-            cname = cy.cy_cname.invoke(name)
-        except gdb.GdbError:
-            cname = name
-       
-        gdb.execute('print ' + cname)
-        
+        gdb.execute('print ' + self.cy.cy_cname.invoke(name, string=True))
     
     def complete(self):
         if self.is_cython_function():
             f = self.get_cython_function()
             return list(itertools.chain(f.locals, f.globals))
-        return []
+        else:
+            return []
 
 
 class CyLocals(CythonCommand):
+    """
+    List the locals from the current Cython frame.
+    """
+    
+    name = 'cy locals'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
     def ns(self):
         return self.get_cython_function().locals
     
-    @require_cython_frame
+    @dispatch_on_frame(c_command='info locals', python_command='py-locals')
     def invoke(self, name, from_tty):
         try:
             ns = self.ns()
@@ -682,10 +771,18 @@ class CyLocals(CythonCommand):
 
 
 class CyGlobals(CythonCommand):
+    """
+    List the globals from the current Cython module.
+    """
+    
+    name = 'cy globals'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
     def ns(self):
         return self.get_cython_function().globals
     
-    @require_cython_frame
+    @dispatch_on_frame(c_command='info variables', python_command='py-globals')
     def invoke(self, name, from_tty):
         # include globals from the debug info XML file!
         m = gdb.parse_and_eval('__pyx_m')
@@ -709,26 +806,54 @@ class CyGlobals(CythonCommand):
 
 class CyCName(gdb.Function, CythonBase):
     """
-    Get the C name of a Cython variable.
+    Get the C name of a Cython variable in the current context.
+    Examples:
+        
+        print $cy_cname("function")
+        print $cy_cname("Class.method")
+        print $cy_cname("module.function")
     """
     
     @require_cython_frame
-    def invoke(self, cyname, frame=None):
+    def invoke(self, cyname, string=False, frame=None):
         frame = frame or gdb.selected_frame()
         cname = None
         
-        cyname = cyname.string()
+        if isinstance(cyname, gdb.Value):
+            # convert to a python string so it supports proper hashing
+            cyname = cyname.string()
+        
         if self.is_cython_function(frame):
             cython_function = self.get_cython_function(frame)
             if cyname in cython_function.locals:
                 cname = cython_function.locals[cyname].cname
             elif cyname in cython_function.module.globals:
                 cname = cython_function.module.globals[cyname].cname
-        
+            else:
+                qname = '%s.%s' % (cython_function.module.name, cyname)
+                if qname in cython_function.module.functions:
+                    cname = cython_function.module.functions[qname].cname
+            
+        if not cname:
+            cname = self.cy.functions_by_qualified_name.get(cyname)
+            
         if not cname:
             raise gdb.GdbError('No such Cython variable: %s' % cyname)
         
-        return cname
-        
+        if string:
+            return cname
+        else:
+            return gdb.parse_and_eval(cname)
+
+
+class CyLine(gdb.Function, CythonBase):
+    """
+    Get the current Cython line.
+    """
+    
+    @require_cython_frame
+    def invoke(self):
+        return self.get_cython_lineno()
+
 
-cy = CyCy()
\ No newline at end of file
+cy = CyCy.register()
\ No newline at end of file
index 323ae0cf90f83282c571e729a1a3b70e2f116117..836868d122cf6a402453d15163525052f0337b81 100644 (file)
@@ -44,8 +44,12 @@ the type names are known to the debugger
 The module also extends gdb with some python-specific commands.
 '''
 from __future__ import with_statement
-import gdb
+
+import os
 import sys
+import tempfile
+
+import gdb
 
 # Look up the gdb.Type for some standard types:
 _type_char_ptr = gdb.lookup_type('char').pointer() # char*
@@ -1448,3 +1452,165 @@ class PyLocals(gdb.Command):
                       pyop_value.get_truncated_repr(MAX_OUTPUT_LEN)))
 
 PyLocals()
+
+
+def execute(command, from_tty=False, to_string=False):
+    """
+    Replace gdb.execute() with this function and have it accept a 'to_string'
+    argument (new in 7.2). Have it properly capture stderr also.
+    
+    Unfortuntaly, this function is not reentrant.
+    """
+    if not to_string:
+        return _execute(command, from_tty)
+            
+    fd, filename = tempfile.mkstemp()
+    
+    try:
+        _execute("set logging file %s" % filename)
+        _execute("set logging redirect on")
+        _execute("set logging on")
+        _execute("set pagination off")
+        
+        _execute(command, from_tty)
+    finally:
+        data = os.fdopen(fd).read()
+        os.remove(filename)
+        _execute("set logging off")
+        _execute("set pagination on")
+        return data
+        
+_execute = gdb.execute
+gdb.execute = execute
+
+
+class GenericCodeStepper(gdb.Command):
+    """
+    Superclass for code stepping. Subclasses must implement the following 
+    methods:
+        
+        lineno(frame)               - tells the current line number (only
+                                      called for a relevant frame)
+        is_relevant_function(frame) - tells whether we care about frame 'frame'
+        get_source_line(frame)      - get the line of source code for the 
+                                      current line (only called for a relevant
+                                      frame)
+        
+    This class provides an 'invoke' method that invokes a 'step' or 'step-over'
+    depending on the 'stepper' argument.
+    """
+    
+    def __init__(self, name, stepper=False):
+        super(GenericCodeStepper, self).__init__(name, 
+                                                 gdb.COMMAND_RUNNING,
+                                                 gdb.COMPLETE_NONE)
+        self.stepper = stepper
+    
+    def _init_stepping(self):
+        self.beginframe = gdb.selected_frame()
+        self.beginline = self.lineno(self.beginframe)
+        if not self.stepper:
+            self.depth = self._stackdepth(self.beginframe)
+
+    def _next_step(self, gdb_command):
+        """
+        Teturns whether to continue stepping. This method sets the instance 
+        attributes 'result' and 'stopped_running'. 'result' hold the output
+        of the executed gdb command ('step' or 'next')
+        """
+        self.result = gdb.execute(gdb_command, to_string=True)
+        self.stopped_running = gdb.inferiors()[0].pid == 0
+        
+        if self.stopped_running:
+            # We stopped running
+            return False
+            
+        newframe = gdb.selected_frame()
+        
+        hit_breakpoint = self.result.startswith('Breakpoint')
+        is_relevant_function = self.is_relevant_function(newframe)
+        
+        if newframe != self.beginframe:
+            # new function
+            if not self.stepper:
+                is_relevant_function = (
+                    self._stackdepth(newframe) < self.depth and
+                    is_relevant_function)
+
+            new_lineno = False
+        else:
+            is_relevant_function = False
+            new_lineno = self.lineno(newframe) > self.beginline
+        
+        return not (hit_breakpoint or new_lineno or is_relevant_function)
+        
+    def _end_stepping(self):
+        # sys.stdout.write(self.result)
+        
+        if not self.stopped_running:
+            frame = gdb.selected_frame()
+            if self.is_relevant_function(frame):
+                print self.get_source_line(frame)
+    
+    def _stackdepth(self, frame):
+        depth = 0
+        while frame:
+            frame = frame.older()
+            depth += 1
+            
+        return depth
+    
+    def invoke(self, args, from_tty):
+        if args:
+            nsteps = int(args)
+        else:
+            nsteps = 1
+        
+        if self.stepper:
+            gdb_command = 'step'
+        else:
+            gdb_command= 'next'
+        
+        for nthstep in xrange(nsteps):
+            self._init_stepping()
+            while self._next_step(gdb_command):
+                pass
+            
+            self._end_stepping()
+
+
+class PythonCodeStepper(GenericCodeStepper):
+    
+    def pyframe(self, frame):
+        pyframe = Frame(frame).get_pyop()
+        if pyframe:
+            return pyframe
+        else:
+            raise gdb.GdbError(
+                "Unable to find the Python frame, run your code with a debug "
+                "build (configure with --with-pydebug or compile with -g).")
+        
+    def lineno(self, frame):
+        return self.pyframe(frame).current_line_num()
+    
+    def is_relevant_function(self, frame):
+        return Frame(frame).is_evalframeex()
+
+    def get_source_line(self, frame):
+        try:
+            return self.pyframe(frame).current_line().rstrip()
+        except IOError, e:
+            gdb.GdbError('Unable to retrieve source code: %s' % (e,))
+
+
+class PyStep(PythonCodeStepper):
+    "Step through Python code."
+
+class PyNext(PythonCodeStepper):
+    "Step-over Python code."
+
+py_step = PyStep('py-step', stepper=True)
+py_next = PyNext('py-next', stepper=False)
+
+class PyShowCCode(gdb.Parameter):
+    pass
\ No newline at end of file
index 7a7f5158261fa4b2afd2a30f1a349fd3a90fce1c..e573531d79d4850dd0bda2129b7400b265bba320 100755 (executable)
--- a/bin/cygdb
+++ b/bin/cygdb
@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb
 
 if __name__ == '__main__':
     if len(sys.argv) > 1:
-        cygdb.main(path_to_debug_info=sys.argv[1], 
-                   gdb_argv=sys.argv[2:])
+        path_to_debug_info = sys.argv[1]
+        
+        no_import = False
+        if path_to_debug_info == '--':
+            no_import = True
+        
+        cygdb.main(path_to_debug_info, 
+                   gdb_argv=sys.argv[2:],
+                   no_import=no_import)
     else:
         cygdb.main()
index 7a7f5158261fa4b2afd2a30f1a349fd3a90fce1c..e573531d79d4850dd0bda2129b7400b265bba320 100644 (file)
--- a/cygdb.py
+++ b/cygdb.py
@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb
 
 if __name__ == '__main__':
     if len(sys.argv) > 1:
-        cygdb.main(path_to_debug_info=sys.argv[1], 
-                   gdb_argv=sys.argv[2:])
+        path_to_debug_info = sys.argv[1]
+        
+        no_import = False
+        if path_to_debug_info == '--':
+            no_import = True
+        
+        cygdb.main(path_to_debug_info, 
+                   gdb_argv=sys.argv[2:],
+                   no_import=no_import)
     else:
         cygdb.main()