'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)
@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()
@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
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()
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):
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
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):
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()
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):
"""
cy import
cy break
cy step
+ cy next
cy print
cy list
cy locals
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 = {}
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 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)
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. "
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)
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()
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')
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
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*
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