Disable watchpoint stepping by default (use cy step -w or cy step --watchpoint to...
[cython.git] / Cython / Debugger / libcython.py
index c2369fa16b566309d77719825befcc43f62f8226..4648d522a45448b143dc3ebe98fd0dd80e5168e0 100644 (file)
@@ -2,8 +2,12 @@
 GDB extension that adds Cython support.
 """
 
+from __future__ import with_statement
+
+import os
 import sys
 import textwrap
+import operator
 import traceback
 import functools
 import itertools
@@ -45,30 +49,17 @@ else:
 
 from Cython.Debugger import libpython
 
-
-# Cython module namespace
-cython_namespace = {}
-
 # C or Python type
-CObject = object()
-PythonObject = object()
-
-# maps (unique) qualified function names (e.g. 
-# cythonmodule.ClassName.method_name) to the CythonFunction object
-functions_by_qualified_name = {}
-
-# unique cnames of Cython functions
-functions_by_cname = {}
-
-# map function names like method_name to a list of all such CythonFunction
-# objects
-functions_by_name = collections.defaultdict(list)
+CObject = 'CObject'
+PythonObject = 'PythonObject'
 
+_data_types = dict(CObject=CObject, PythonObject=PythonObject)
 _filesystemencoding = sys.getfilesystemencoding() or 'UTF-8'
 
 # decorators
 
 def dont_suppress_errors(function):
+    "*sigh*, readline"
     @functools.wraps(function)
     def wrapper(*args, **kwargs):
         try:
@@ -79,14 +70,72 @@ def dont_suppress_errors(function):
     
     return wrapper
 
-def default_selected_gdb_frame(function):
+def default_selected_gdb_frame(err=True):
+    def decorator(function):
+        @functools.wraps(function)
+        def wrapper(self, frame=None, *args, **kwargs):
+            try:
+                frame = frame or gdb.selected_frame()
+            except RuntimeError:
+                raise gdb.GdbError("No frame is currently selected.")
+                
+            if err and frame.name() is None:
+                raise NoFunctionNameInFrameError()
+    
+            return function(self, frame, *args, **kwargs)
+        return wrapper
+    return decorator
+
+def require_cython_frame(function):
     @functools.wraps(function)
-    def wrapper(self, frame=None, **kwargs):
-        frame = frame or gdb.selected_frame()
-        if frame.name() is None:
-            raise NoFunctionNameInFrameError()
+    @require_running_program
+    def wrapper(self, *args, **kwargs):
+        frame = kwargs.get('frame') or gdb.selected_frame()
+        if not self.is_cython_function(frame):
+            raise gdb.GdbError('Selected frame does not correspond with a '
+                               '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
 
-        return function(self, frame)
+def require_running_program(function):
+    @functools.wraps(function)
+    def wrapper(*args, **kwargs):
+        try:
+            gdb.selected_frame()
+        except RuntimeError:
+            raise gdb.GdbError("No frame is currently selected.")
+        
+        return function(*args, **kwargs)
+    return wrapper
+    
+
+def gdb_function_value_to_unicode(function):
+    @functools.wraps(function)
+    def wrapper(self, string, *args, **kwargs):
+        if isinstance(string, gdb.Value):
+            string = string.string()
+
+        return function(self, string, *args, **kwargs)
     return wrapper
 
 
@@ -94,23 +143,28 @@ def default_selected_gdb_frame(function):
 # 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.functions = {}
+        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)
 
 class CythonVariable(object):
 
-    def __init__(self, name, cname, qualified_name, type):
+    def __init__(self, name, cname, qualified_name, type, lineno):
         self.name = name
         self.cname = cname
         self.qualified_name = qualified_name
         self.type = type
+        self.lineno = int(lineno)
 
 class CythonFunction(CythonVariable):
     def __init__(self, 
@@ -124,18 +178,225 @@ class CythonFunction(CythonVariable):
         super(CythonFunction, self).__init__(name, 
                                              cname, 
                                              qualified_name, 
-                                             type)
+                                             type,
+                                             lineno)
         self.module = module
         self.pf_cname = pf_cname
-        self.lineno = lineno
         self.locals = {}
         self.arguments = []
         self.step_into_functions = set()
 
+
+# General purpose classes
+
+class CythonBase(object):
+    
+    @default_selected_gdb_frame(err=False)
+    def is_cython_function(self, frame):
+        return frame.name() in self.cy.functions_by_cname
+
+    @default_selected_gdb_frame(err=False)
+    def is_python_function(self, frame):
+        """
+        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()
+
+    @default_selected_gdb_frame()
+    def get_c_lineno(self, frame):
+        return frame.find_sal().line
+    
+    @default_selected_gdb_frame()
+    def get_cython_function(self, frame):
+        result = self.cy.functions_by_cname.get(frame.name())
+        if result is None:
+            raise NoCythonFunctionInFrameError()
+            
+        return result
+    
+    @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), 0)
+    
+    @default_selected_gdb_frame()
+    def get_source_desc(self, frame):
+        filename = lineno = lexer = None
+        if self.is_cython_function(frame):
+            filename = self.get_cython_function(frame).module.filename
+            lineno = self.get_cython_lineno(frame)
+            if pygments:
+                lexer = pygments.lexers.CythonLexer(stripall=False)
+        elif self.is_python_function(frame):
+            pyframeobject = libpython.Frame(frame).get_pyop()
+
+            if not pyframeobject:
+                raise gdb.GdbError('Unable to read information on python frame')
+
+            filename = pyframeobject.filename()
+            lineno = pyframeobject.current_line_num()
+            
+            if pygments:
+                lexer = pygments.lexers.PythonLexer(stripall=False)
+        else:
+            symbol_and_line_obj = frame.find_sal()
+            if not symbol_and_line_obj or not symbol_and_line_obj.symtab:
+                filename = None
+                lineno = 0
+            else:
+                filename = symbol_and_line_obj.symtab.fullname()
+                lineno = symbol_and_line_obj.line
+                if pygments:
+                    lexer = pygments.lexers.CLexer(stripall=False)
+            
+        return SourceFileDescriptor(filename, lexer), lineno
+
+    @default_selected_gdb_frame()
+    def get_source_line(self, frame):
+        source_desc, lineno = self.get_source_desc()
+        return source_desc.get_source(lineno)
+    
+    @default_selected_gdb_frame()
+    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 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
+    
+    @default_selected_gdb_frame(err=False)
+    def print_stackframe(self, frame, index, is_c=False):
+        """
+        Print a C, Cython or Python stack frame and the line of source code
+        if available.
+        """
+        # do this to prevent the require_cython_frame decorator from
+        # raising GdbError when calling self.cy.cy_cvalue.invoke()
+        selected_frame = gdb.selected_frame()
+        frame.select()
+        
+        try:
+            source_desc, lineno = self.get_source_desc(frame)
+        except NoFunctionNameInFrameError:
+            print '#%-2d Unknown Frame (compile with -g)' % index
+            return
+
+        if not is_c and self.is_python_function(frame):
+            pyframe = libpython.Frame(frame).get_pyop()
+            if pyframe is None or pyframe.is_optimized_out():
+                # print this python function as a C function
+                return self.print_stackframe(frame, index, is_c=True)
+            
+            func_name = pyframe.co_name
+            func_cname = 'PyEval_EvalFrameEx'
+            func_args = []
+        elif self.is_cython_function(frame):
+            cyfunc = self.get_cython_function(frame)
+            f = lambda arg: self.cy.cy_cvalue.invoke(arg, frame=frame)
+            
+            func_name = cyfunc.name
+            func_cname = cyfunc.cname
+            func_args = [] # [(arg, f(arg)) for arg in cyfunc.arguments]
+        else:
+            source_desc, lineno = self.get_source_desc(frame)
+            func_name = frame.name()
+            func_cname = func_name
+            func_args = []
+        
+        try:
+            gdb_value = gdb.parse_and_eval(func_cname)
+        except RuntimeError:
+            func_address = 0
+        else:
+            # Seriously? Why is the address not an int?
+            func_address = int(str(gdb_value.address).split()[0], 0)
+        
+        a = ', '.join('%s=%s' % (name, val) for name, val in func_args)
+        print '#%-2d 0x%016x in %s(%s)' % (index, func_address, func_name, a),
+            
+        if source_desc.filename is not None:
+            print 'at %s:%s' % (source_desc.filename, lineno),
+        
+        print
+            
+        try:
+            print '    ' + source_desc.get_source(lineno)
+        except gdb.GdbError:
+            pass
+        
+        selected_frame.select()
+    
+    def get_remote_cython_globals_dict(self):
+        m = gdb.parse_and_eval('__pyx_m')
+        
+        try:
+            PyModuleObject = gdb.lookup_type('PyModuleObject')
+        except RuntimeError:
+            raise gdb.GdbError(textwrap.dedent("""\
+                Unable to lookup type PyModuleObject, did you compile python 
+                with debugging support (-g)?"""))
+            
+        m = m.cast(PyModuleObject.pointer())
+        return m['md_dict']
+        
+    
+    def get_cython_globals_dict(self):
+        """
+        Get the Cython globals dict where the remote names are turned into
+        local strings.
+        """
+        remote_dict = self.get_remote_cython_globals_dict()
+        pyobject_dict = libpython.PyObjectPtr.from_pyobject_ptr(remote_dict)
+        
+        result = {}
+        seen = set()
+        for k, v in pyobject_dict.iteritems():
+            result[k.proxyval(seen)] = v
+            
+        return result
+
+    def print_gdb_value(self, name, value, max_name_length=None, prefix=''):
+        if libpython.pretty_printer_lookup(value):
+            typename = ''
+        else:
+            typename = '(%s) ' % (value.type,)
+                
+        if max_name_length is None:
+            print '%s%s = %s%s' % (prefix, name, typename, value)
+        else:
+            print '%s%-*s = %s%s' % (prefix, max_name_length, name, typename, 
+                                     value)
+
+    def is_initialized(self, cython_func, local_name):
+        cur_lineno = self.get_cython_lineno()
+        return (local_name in cython_func.arguments or
+                (local_name in cython_func.locals and
+                 cur_lineno > cython_func.locals[local_name].lineno))
+
 class SourceFileDescriptor(object):
-    def __init__(self, filename, lineno, lexer, formatter=None):
+    def __init__(self, filename, lexer, formatter=None):
         self.filename = filename
-        self.lineno = lineno
         self.lexer = lexer
         self.formatter = formatter
 
@@ -143,8 +404,8 @@ class SourceFileDescriptor(object):
         return self.filename is not None
 
     def lex(self, code):
-        if pygments and parameter.colorize_code:
-            bg = parameter.terminal_background.value
+        if pygments and self.lexer and parameters.colorize_code:
+            bg = parameters.terminal_background.value
             if self.formatter is None:
                 formatter = pygments.formatters.TerminalFormatter(bg=bg)
             else:
@@ -154,24 +415,44 @@ class SourceFileDescriptor(object):
 
         return code
 
-    def get_source(self, start=0, stop=None, lex_source=True):
-        # todo: have it detect the source file's encoding
-        if not self.filename:
-            return 'Unable to retrieve source code'
-
-        start = max(self.lineno + start, 0)
-        if stop is None:
-            stop = self.lineno + 1
-        else:
-            stop = self.lineno + stop
-            
+    def _get_source(self, start, stop, lex_source, mark_line, lex_entire):
         with open(self.filename) as f:
-            source = itertools.islice(f, start, stop)
+            # to provide "correct" colouring, the entire code needs to be
+            # lexed. However, this makes a lot of things terribly slow, so
+            # we decide not to. Besides, it's unlikely to matter.
             
-            if lex_source:
-                return [self.lex(line) for line in source]
-            else:
-                return list(source)
+            if lex_source and lex_entire:
+                f = self.lex(f.read()).splitlines()
+            
+            slice = itertools.islice(f, start - 1, stop - 1)
+            
+            for idx, line in enumerate(slice):
+                if start + idx == mark_line:
+                    prefix = '>'
+                else:
+                    prefix = ' '
+                
+                if lex_source and not lex_entire:
+                    line = self.lex(line)
+
+                yield '%s %4d    %s' % (prefix, start + idx, line.rstrip())
+
+    def get_source(self, start, stop=None, lex_source=True, mark_line=0, 
+                   lex_entire=False):
+        exc = gdb.GdbError('Unable to retrieve source code')
+        
+        if not self.filename:
+            raise exc
+        
+        start = max(start, 1)
+        if stop is None:
+            stop = start + 1
+
+        try:
+            return '\n'.join(
+                self._get_source(start, stop, lex_source, mark_line, lex_entire))
+        except IOError:
+            raise exc
 
 
 # Errors
@@ -227,98 +508,139 @@ 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 Parameter(object):
+
+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)
-    """
-    complete_unqualified = CompleteUnqualifiedFunctionNames(
-        'cy_complete_unqualified',
-        gdb.COMMAND_BREAKPOINTS,
-        gdb.PARAM_BOOLEAN,
-        True)
-    colorize_code = ColorizeSourceCode(
-        'cy_colorize_code',
-        gdb.COMMAND_FILES,
-        gdb.PARAM_BOOLEAN,
-        True)
-    terminal_background = TerminalBackground(
-        'cy_terminal_background_color',
-        gdb.COMMAND_FILES,
-        gdb.PARAM_STRING,
-        "dark")
-
-parameter = Parameter()
+    future (mostly to remind us that we're dealing with parameters).
+    """
+    
+    def __init__(self):
+        self.complete_unqualified = CompleteUnqualifiedFunctionNames(
+            'cy_complete_unqualified',
+            gdb.COMMAND_BREAKPOINTS,
+            gdb.PARAM_BOOLEAN,
+            True)
+        self.colorize_code = ColorizeSourceCode(
+            'cy_colorize_code',
+            gdb.COMMAND_FILES,
+            gdb.PARAM_BOOLEAN,
+            True)
+        self.terminal_background = TerminalBackground(
+            'cy_terminal_background_color',
+            gdb.COMMAND_FILES,
+            gdb.PARAM_STRING,
+            "dark")
+        
+parameters = CythonParameters()
+
 
 # Commands
 
-class CythonCommand(gdb.Command):
+class CythonCommand(gdb.Command, CythonBase):
+    """
+    Base class for Cython commands
+    """
+    
+    command_class = gdb.COMMAND_NONE
+    
+    @classmethod
+    def _register(cls, clsname, args, kwargs):
+        if not hasattr(cls, 'completer_class'):
+            return cls(clsname, cls.command_class, *args, **kwargs)
+        else:
+            return cls(clsname, cls.command_class, cls.completer_class, 
+                       *args, **kwargs)
+    
+    @classmethod
+    def register(cls, *args, **kwargs):
+        alias = getattr(cls, 'alias', None)
+        if alias:
+            cls._register(cls.alias, args, kwargs)
+            
+        return cls._register(cls.name, args, kwargs)
+
+
+class CyCy(CythonCommand):
     """
     Invoke a Cython command. Available commands are:
         
         cy import
         cy break
         cy step
-        cy print
+        cy next
+        cy run
+        cy cont
+        cy finish
+        cy up
+        cy down
+        cy select
+        cy bt / cy backtrace
         cy list
+        cy print
         cy locals
         cy globals
-        cy backtrace
-        cy info line
+        cy exec
     """
     
-    def is_cython_function(self, frame=None):
-        func_name = (frame or gdb.selected_frame()).name()
-        return func_name is not None and func_name in functions_by_cname
-
-    @default_selected_gdb_frame
-    def is_python_function(self, frame):
-        return libpython.Frame(frame).is_evalframeex()
-
-    @default_selected_gdb_frame
-    def get_c_function_name(self, frame):
-        return frame.name()
-
-    @default_selected_gdb_frame
-    def get_c_lineno(self, frame):
-        return frame.find_sal().line
+    name = 'cy'
+    command_class = gdb.COMMAND_NONE
+    completer_class = gdb.COMPLETE_COMMAND
     
-    @default_selected_gdb_frame
-    def get_cython_function(self, frame):
-        result = functions_by_cname.get(frame.name())
-        if result is None:
-            raise NoCythonFunctionInFrameError()
+    def __init__(self, name, command_class, completer_class):
+        # keep the signature 2.5 compatible (i.e. do not use f(*a, k=v)
+        super(CythonCommand, self).__init__(name, command_class, 
+                                            completer_class, prefix=True)
+        
+        commands = dict(
+            import_ = CyImport.register(),
+            break_ = CyBreak.register(),
+            step = CyStep.register(),
+            next = CyNext.register(),
+            run = CyRun.register(),
+            cont = CyCont.register(),
+            finish = CyFinish.register(),
+            up = CyUp.register(),
+            down = CyDown.register(),
+            select = CySelect.register(),
+            bt = CyBacktrace.register(),
+            list = CyList.register(),
+            print_ = CyPrint.register(),
+            locals = CyLocals.register(),
+            globals = CyGlobals.register(),
+            exec_ = libpython.FixGdbCommand('cy exec', '-cy-exec'),
+            _exec = CyExec.register(),
+            cy_cname = CyCName('cy_cname'),
+            cy_cvalue = CyCValue('cy_cvalue'),
+            cy_lineno = CyLine('cy_lineno'),
+        )
             
-        return result
-    
-    @default_selected_gdb_frame
-    def get_cython_lineno(self, frame):
-        cyfunc = self.get_cython_function(frame)
-        return cyfunc.module.lineno_c2cy.get(self.get_c_lineno(frame))
-
-    @default_selected_gdb_frame
-    def get_source_desc(self, frame):
-        if self.is_cython_function():
-            filename = self.get_cython_function(frame).module.filename
-            lineno = self.get_cython_lineno(frame)
-            lexer = pygments.lexers.CythonLexer()
-        else:
-            filename = None
-            lineno = -1
-            lexer = None
-
-        return SourceFileDescriptor(filename, lineno, lexer)
-
-   
-cy = CythonCommand('cy', gdb.COMMAND_NONE, gdb.COMPLETE_COMMAND, prefix=True)
+        for command_name, command in commands.iteritems():
+            command.cy = self
+            setattr(self, command_name, command)
+        
+        self.cy = self
+        
+        # Cython module namespace
+        self.cython_namespace = {}
+        
+        # maps (unique) qualified function names (e.g. 
+        # cythonmodule.ClassName.method_name) to the CythonFunction object
+        self.functions_by_qualified_name = {}
+        
+        # unique cnames of Cython functions
+        self.functions_by_cname = {}
+        
+        # map function names like method_name to a list of all such 
+        # CythonFunction objects
+        self.functions_by_name = collections.defaultdict(list)
 
 
 class CyImport(CythonCommand):
@@ -326,21 +648,25 @@ 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)
             
             for module in t.getroot():
                 cython_module = CythonModule(**module.attrib)
-                cython_namespace[cython_module.name] = cython_module
+                self.cy.cython_namespace[cython_module.name] = cython_module
                 
                 for variable in module.find('Globals'):
                     d = variable.attrib
@@ -349,15 +675,18 @@ class CyImport(CythonCommand):
                 for function in module.find('Functions'):
                     cython_function = CythonFunction(module=cython_module, 
                                                      **function.attrib)
-                    cython_module.functions[cython_function.name] = \
-                        cython_function
-                    
+
                     # update the global function mappings
-                    functions_by_name[cython_function.name].append(
-                        cython_function)
-                    functions_by_qualified_name[
+                    name = cython_function.name
+                    qname = cython_function.qualified_name
+                    
+                    self.cy.functions_by_name[name].append(cython_function)
+                    self.cy.functions_by_qualified_name[
                         cython_function.qualified_name] = cython_function
-                    functions_by_cname[cython_function.cname] = cython_function
+                    self.cy.functions_by_cname[
+                        cython_function.cname] = cython_function
+                    
+                    d = cython_module.functions[qname] = cython_function
                     
                     for local in function.find('Locals'):
                         d = local.attrib
@@ -376,9 +705,8 @@ class CyImport(CythonCommand):
                     cython_module.lineno_cy2c[cython_lineno] = min(c_linenos)
                     for c_lineno in c_linenos:
                         cython_module.lineno_c2cy[c_lineno] = cython_lineno
-                    
-                
-cy.import_ = CyImport('cy import', gdb.COMMAND_STATUS, gdb.COMPLETE_FILENAME)
+
+        self.cy.step.init_breakpoints()
 
 
 class CyBreak(CythonCommand):
@@ -394,25 +722,42 @@ class CyBreak(CythonCommand):
     or for a line number:
     
         cy break cython_module:lineno...
+    
+    Set a Python breakpoint:
+        Break on any function or method named 'func' in module 'modname'
+            
+            cy break -p modname.func...
+        
+        Break on any function or method named 'func'
+            
+            cy break -p func...
     """
     
+    name = 'cy break'
+    command_class = gdb.COMMAND_BREAKPOINTS
+    
     def _break_pyx(self, name):
         modulename, _, lineno = name.partition(':')
         lineno = int(lineno)
-        cython_module = cython_namespace[modulename]
+        if modulename:
+            cython_module = self.cy.cython_namespace[modulename]
+        else:
+            cython_module = self.get_cython_function().module
+
         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:
-            sys.stderr.write("Not a valid line number (does it contain actual code?)\n")
+            raise GdbError("Not a valid line number. "
+                           "Does it contain actual code?")
     
     def _break_funcname(self, funcname):
-        func = functions_by_qualified_name.get(funcname)
+        func = self.cy.functions_by_qualified_name.get(funcname)
         break_funcs = [func]
         
         if not func:
-            funcs = functions_by_name.get(funcname)
+            funcs = self.cy.functions_by_name.get(funcname)
             if not funcs:
                 gdb.execute('break ' + funcname)
                 return
@@ -451,21 +796,31 @@ class CyBreak(CythonCommand):
                 gdb.execute('break %s' % func.pf_cname)
     
     def invoke(self, function_names, from_tty):
-        for funcname in string_to_argv(function_names.encode('UTF-8')):
-            if ':' in funcname:
+        argv = string_to_argv(function_names.encode('UTF-8'))
+        if function_names.startswith('-p'):
+            argv = argv[1:]
+            python_breakpoints = True
+        else:
+            python_breakpoints = False
+        
+        for funcname in argv:
+            if python_breakpoints:
+                gdb.execute('py-break %s' % funcname)
+            elif ':' in funcname:
                 self._break_pyx(funcname)
             else:
                 self._break_funcname(funcname)
     
     @dont_suppress_errors
     def complete(self, text, word):
-        names = functions_by_qualified_name
-        if parameter.complete_unqualified:
-            names = itertools.chain(names, functions_by_name)
+        names = self.cy.functions_by_qualified_name
+        if parameters.complete_unqualified:
+            names = itertools.chain(names, self.cy.functions_by_name)
 
         words = text.strip().split()
         if words and '.' in words[-1]:
-            compl = [n for n in functions_by_qualified_name 
+            lastword = words[-1]
+            compl = [n for n in self.cy.functions_by_qualified_name 
                            if n.startswith(lastword)]
         else:
             seen = set(text[:-len(word)].split())
@@ -479,136 +834,465 @@ class CyBreak(CythonCommand):
             
         return compl
 
-cy.break_ = CyBreak('cy break', gdb.COMMAND_BREAKPOINTS)
 
+class CythonInfo(CythonBase, libpython.PythonInfo):
+    """
+    Implementation of the interface dictated by libpython.LanguageInfo.
+    """
+    
+    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)
+        return super(CythonInfo, self).lineno(frame)
+    
+    def get_source_line(self, frame):
+        try:
+            line = super(CythonInfo, self).get_source_line(frame)
+        except gdb.GdbError:
+            return None
+        else:
+            return line.strip() or None
 
-class CyStep(CythonCommand):
+    def exc_info(self, frame):
+        if self.is_python_function:
+            return super(CythonInfo, self).exc_info(frame)
 
-    def step(self, from_tty=True, nsteps=1):
-        for nthstep in xrange(nsteps):
-            cython_func = self.get_cython_function()
-            beginline = self.get_cython_lineno()
-            curframe = gdb.selected_frame() 
+    def runtime_break_functions(self):
+        if self.is_cython_function():
+            return self.get_cython_function().step_into_functions
     
-            while True:
-                result = gdb.execute('step', False, True)
-                if result.startswith('Breakpoint'):
-                        break
-                newframe = gdb.selected_frame()
-                if newframe == curframe:
-                    # still in the same function
-                    if self.get_cython_lineno() > beginline:
-                        break
-                else:
-                    # 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
-        
-        line, = self.get_source_desc().get_source()
-        sys.stdout.write(line)
+    def static_break_functions(self):
+        result = ['PyEval_EvalFrameEx']
+        result.extend(self.cy.functions_by_cname)
+        return result
 
-    def invoke(self, steps, from_tty):
-        if self.is_cython_function():
-            if steps:
-                self.step(from_tty, int(steps))
+
+class CythonExecutionControlCommand(CythonCommand, 
+                                    libpython.ExecutionControlCommandBase):
+    
+    @classmethod
+    def register(cls):
+        return cls(cls.name, cython_info)
+
+
+class CyStep(CythonExecutionControlCommand, libpython.PythonStepperMixin):
+    "Step through Cython, Python or C code."
+    
+    name = 'cy step'
+    stepinto = True
+    
+    def invoke(self, args, from_tty):
+        if self.is_python_function():
+            self.python_step(args, self.stepinto)
+        elif not self.is_cython_function():
+            if self.stepinto:
+                command = 'step'
             else:
-                self.step(from_tty)
+                command = 'next'
+                
+            self.finish_executing(gdb.execute(command, to_string=True))
         else:
-            gdb.execute('step ' + steps)
+            self.step(stepinto=self.stepinto)
+
+
+class CyNext(CyStep):
+    "Step-over Python code."
+
+    name = 'cy next'
+    stepinto = False
+
+
+class CyRun(CythonExecutionControlCommand):
+    """
+    Run a Cython program. This is like the 'run' command, except that it 
+    displays Cython or Python source lines as well
+    """
+    
+    name = 'cy run'
+    
+    invoke = CythonExecutionControlCommand.run
+
 
-cy.step = CyStep('cy step', gdb.COMMAND_RUNNING, gdb.COMPLETE_NONE)
+class CyCont(CythonExecutionControlCommand):
+    """
+    Continue a Cython program. This is like the 'run' command, except that it 
+    displays Cython or Python source lines as well.
+    """
+    
+    name = 'cy cont'
+    invoke = CythonExecutionControlCommand.cont
+
+
+class CyFinish(CythonExecutionControlCommand):
+    """
+    Execute until the function returns.
+    """
+    name = 'cy finish'
+
+    invoke = CythonExecutionControlCommand.finish
+
+
+class CyUp(CythonCommand):
+    """
+    Go up a Cython, Python or relevant C frame.
+    """
+    name = 'cy up'
+    _command = 'up'
+    
+    def invoke(self, *args):
+        try:
+            gdb.execute(self._command, to_string=True)
+            while not self.is_relevant_function(gdb.selected_frame()):
+                gdb.execute(self._command, to_string=True)
+        except RuntimeError, e:
+            raise gdb.GdbError(*e.args)
+        
+        frame = gdb.selected_frame()
+        index = 0
+        while frame:
+            frame = frame.older()
+            index += 1
+            
+        self.print_stackframe(index=index - 1)
+
+
+class CyDown(CyUp):
+    """
+    Go down a Cython, Python or relevant C frame.
+    """
+    
+    name = 'cy down'
+    _command = 'down'
+
+
+class CySelect(CythonCommand):
+    """
+    Select a frame. Use frame numbers as listed in `cy backtrace`.
+    This command is useful because `cy backtrace` prints a reversed backtrace.
+    """
+    
+    name = 'cy select'
+    
+    def invoke(self, stackno, from_tty):
+        try:
+            stackno = int(stackno)
+        except ValueError:
+            raise gdb.GdbError("Not a valid number: %r" % (stackno,))
+        
+        frame = gdb.selected_frame()
+        while frame.newer():
+            frame = frame.newer()
+        
+        stackdepth = libpython.stackdepth(frame)
+        
+        try:
+            gdb.execute('select %d' % (stackdepth - stackno - 1,))
+        except RuntimeError, e:
+            raise gdb.GdbError(*e.args)
+
+
+class CyBacktrace(CythonCommand):
+    'Print the Cython stack'
+    
+    name = 'cy bt'
+    alias = 'cy backtrace'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    @require_running_program
+    def invoke(self, args, from_tty):
+        # get the first frame
+        selected_frame = frame = gdb.selected_frame()
+        while frame.older():
+            frame = frame.older()
+        
+        print_all = args == '-a'
+        
+        index = 0
+        while frame:
+            is_c = False
+            
+            is_relevant = False
+            try:
+                is_relevant = self.is_relevant_function(frame)
+            except CyGDBError:
+                pass
+                
+            if print_all or is_relevant:
+                self.print_stackframe(frame, index)
+            
+            index += 1
+            frame = frame.newer()
+        
+        selected_frame.select()
 
 
 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 = self.get_source_desc()
-        it = enumerate(sd.get_source(-5, +5))
-        sys.stdout.write(
-            ''.join('%4d    %s' % (sd.lineno + i, line) for i, line in it))
-
-cy.list = CyList('cy list', gdb.COMMAND_FILES, gdb.COMPLETE_NONE)
+        sd, lineno = self.get_source_desc()
+        source = sd.get_source(lineno - 5, lineno + 5, mark_line=lineno, 
+                               lex_entire=True)
+        print source
 
 
 class CyPrint(CythonCommand):
     """
     Print a Cython variable using 'cy-print x' or 'cy-print module.function.x'
     """
-   
     
-    def invoke(self, name, from_tty):
-        cname = None
-        if self.is_cython_function():
-            cython_function = self.get_cython_function()
-            if name in cython_function.locals:
-                cname = cython_function.locals[name].cname
-            elif name in cython_function.module.globals:
-                cname = cython_function.module.globals[name].cname
-
-        # let the pretty printers do the work
-        cname = cname or name
-        gdb.execute('print ' + cname)
+    name = 'cy print'
+    command_class = gdb.COMMAND_DATA
     
+    def invoke(self, name, from_tty, max_name_length=None):
+        if self.is_python_function():
+            return gdb.execute('py-print ' + name)
+        elif self.is_cython_function():
+            value = self.cy.cy_cvalue.invoke(name.lstrip('*'))
+            for c in name:
+                if c == '*':
+                    value = value.dereference()
+                else:
+                    break
+                
+            self.print_gdb_value(name, value, max_name_length)
+        else:
+            gdb.execute('print ' + name)
+        
     def complete(self):
         if self.is_cython_function():
-            cf = self.get_cython_function()
-            return list(itertools.chain(cf.locals, cf.globals))
-        return []
-    
-cy.print_ = CyPrint('cy print', gdb.COMMAND_DATA)
+            f = self.get_cython_function()
+            return list(itertools.chain(f.locals, f.globals))
+        else:
+            return []
 
 
+sortkey = lambda (name, value): name.lower()
+
 class CyLocals(CythonCommand):
-    def ns(self):
-        return self.get_cython_function().locals
+    """
+    List the locals from the current Cython frame.
+    """
+    
+    name = 'cy locals'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    @dispatch_on_frame(c_command='info locals', python_command='py-locals')
+    def invoke(self, args, from_tty):
+        local_cython_vars = self.get_cython_function().locals
+        max_name_length = len(max(local_cython_vars, key=len))
+        for name, cyvar in sorted(local_cython_vars.iteritems(), key=sortkey):
+            if self.is_initialized(self.get_cython_function(), cyvar.name):
+                value = gdb.parse_and_eval(cyvar.cname)
+                if not value.is_optimized_out:
+                    self.print_gdb_value(cyvar.name, value, 
+                                         max_name_length, '')
+
+
+class CyGlobals(CyLocals):
+    """
+    List the globals from the current Cython module.
+    """
+    
+    name = 'cy globals'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    @dispatch_on_frame(c_command='info variables', python_command='py-globals')
+    def invoke(self, args, from_tty):
+        global_python_dict = self.get_cython_globals_dict()
+        module_globals = self.get_cython_function().module.globals
         
-    def invoke(self, name, from_tty):
-        try:
-            ns = self.ns()
-        except RuntimeError, e:
-            print e.args[0]
-            return
+        max_globals_len = 0
+        max_globals_dict_len = 0
+        if module_globals:
+            max_globals_len = len(max(module_globals, key=len))
+        if global_python_dict:
+            max_globals_dict_len = len(max(global_python_dict))
+            
+        max_name_length = max(max_globals_len, max_globals_dict_len)
         
-        if ns is None:
-            print ('Information of Cython locals could not be obtained. '
-                   'Is this an actual Cython function and did you '
-                   "'cy import' the debug information?")
+        seen = set()
+        print 'Python globals:'
+        for k, v in sorted(global_python_dict.iteritems(), key=sortkey):
+            v = v.get_truncated_repr(libpython.MAX_OUTPUT_LEN)
+            seen.add(k)
+            print '    %-*s = %s' % (max_name_length, k, v)
         
-        for var in ns.itervalues():
-            val = gdb.parse_and_eval(var.cname)
-            if var.type == PythonObject:
-                result = libpython.PyObjectPtr.from_pyobject_ptr(val)
-            else:
-                result = val
+        print 'C globals:'
+        for name, cyvar in sorted(module_globals.iteritems(), key=sortkey):
+            if name not in seen:
+                try:
+                    value = gdb.parse_and_eval(cyvar.cname)
+                except RuntimeError:
+                    pass
+                else:
+                    if not value.is_optimized_out:
+                        self.print_gdb_value(cyvar.name, value,
+                                             max_name_length, '    ')
+
+
+class CyExec(CythonCommand, libpython.PyExec):
+    """
+    Execute Python code in the nearest Python or Cython frame.
+    """
+    
+    name = '-cy-exec'
+    command_class = gdb.COMMAND_STACK
+    completer_class = gdb.COMPLETE_NONE
+    
+    def _fill_locals_dict(self, executor, local_dict_pointer):
+        "Fill a remotely allocated dict with values from the Cython C stack"
+        cython_func = self.get_cython_function()
+        current_lineno = self.get_cython_lineno()
+        
+        for name, cyvar in cython_func.locals.iteritems():
+            if (cyvar.type == PythonObject and 
+                self.is_initialized(cython_func, name)):
                 
-            print '%s = %s' % (var.name, result)
+                try:
+                    val = gdb.parse_and_eval(cyvar.cname)
+                except RuntimeError:
+                    continue
+                else:
+                    if val.is_optimized_out:
+                        continue
+                
+                pystringp = executor.alloc_pystring(name)
+                code = '''
+                    (PyObject *) PyDict_SetItem(
+                        (PyObject *) %d,
+                        (PyObject *) %d,
+                        (PyObject *) %s)
+                ''' % (local_dict_pointer, pystringp, cyvar.cname)
+
+                try:
+                    if gdb.parse_and_eval(code) < 0:
+                        gdb.parse_and_eval('PyErr_Print()')
+                        raise gdb.GdbError("Unable to execute Python code.")
+                finally:
+                    # PyDict_SetItem doesn't steal our reference
+                    executor.decref(pystringp)
+    
+    def _find_first_cython_or_python_frame(self):
+        frame = gdb.selected_frame()
+        while frame:
+            if (self.is_cython_function(frame) or 
+                self.is_python_function(frame)):
+                return frame
+            
+            frame = frame.older()
+        
+        raise gdb.GdbError("There is no Cython or Python frame on the stack.")
+        
+    def invoke(self, expr, from_tty):
+        frame = self._find_first_cython_or_python_frame()
+        if self.is_python_function(frame):
+            libpython.py_exec.invoke(expr, from_tty)
+            return
+        
+        expr, input_type = self.readcode(expr)
+        executor = libpython.PythonCodeExecutor()
+        
+        with libpython.FetchAndRestoreError():
+            # get the dict of Cython globals and construct a dict in the 
+            # inferior with Cython locals
+            global_dict = gdb.parse_and_eval(
+                '(PyObject *) PyModule_GetDict(__pyx_m)')
+            local_dict = gdb.parse_and_eval('(PyObject *) PyDict_New()')
+            
+            cython_function = self.get_cython_function()
+            
+            try:
+                self._fill_locals_dict(executor, 
+                                       libpython.pointervalue(local_dict))
+                executor.evalcode(expr, input_type, global_dict, local_dict)
+            finally:
+                executor.decref(libpython.pointervalue(local_dict))
 
 
-class CyGlobals(CythonCommand):
-    def ns(self):
-        return self.get_cython_function().globals
+# Functions
+
+class CyCName(gdb.Function, CythonBase):
+    """
+    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")
+    """
     
-    def invoke(self, name, from_tty):
-        # include globals from the debug info XML file!
-        m = gdb.parse_and_eval('__pyx_m')
+    @require_cython_frame
+    @gdb_function_value_to_unicode
+    def invoke(self, cyname, frame=None):
+        frame = frame or gdb.selected_frame()
+        cname = None
         
-        try:
-            PyModuleObject = gdb.lookup_type('PyModuleObject')
-        except RuntimeError:
-            raise gdb.GdbError(textwrap.dedent("""
-                Unable to lookup type PyModuleObject, did you compile python 
-                with debugging support (-g)? If this installation is from your
-                package manager, install python-dbg and run the debug version
-                of python or compile it yourself.
-                """))
+        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
             
-        m = m.cast(PyModuleObject.pointer())
-        d = libpython.PyObjectPtr.from_pyobject_ptr(m['md_dict'])
-        print d.get_truncated_repr(1000)
+        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
+
 
+class CyCValue(CyCName):
+    """
+    Get the value of a Cython variable.
+    """
+    
+    @require_cython_frame
+    @gdb_function_value_to_unicode
+    def invoke(self, cyname, frame=None):
+        try:
+            cname = super(CyCValue, self).invoke(cyname, frame=frame)
+            return gdb.parse_and_eval(cname)
+        except (gdb.GdbError, RuntimeError), e:
+            # variable exists but may not have been initialized yet, or may be
+            # in the globals dict of the Cython module
+            d = self.get_cython_globals_dict()
+            if cyname in d:
+                return d[cyname]._gdbval
+
+            raise gdb.GdbError(str(e))
+
+
+class CyLine(gdb.Function, CythonBase):
+    """
+    Get the current Cython line.
+    """
+    
+    @require_cython_frame
+    def invoke(self):
+        return self.get_cython_lineno()
 
-cy.locals = CyLocals('cy locals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE)
-cy.globals = CyGlobals('cy globals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE)
+cython_info = CythonInfo()
+cy = CyCy.register()
+cython_info.cy = cy
\ No newline at end of file