Commit 1327c85b authored by Mark Florisson's avatar Mark Florisson

dispatch based on frame

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
parent f1e63d52
...@@ -185,7 +185,7 @@ class Context(object): ...@@ -185,7 +185,7 @@ class Context(object):
from ParseTreeTransforms import DebugTransform from ParseTreeTransforms import DebugTransform
self.debug_outputwriter = DebugWriter.CythonDebugWriter( self.debug_outputwriter = DebugWriter.CythonDebugWriter(
options.output_dir) options.output_dir)
debug_transform = [DebugTransform(self)] debug_transform = [DebugTransform(self, options)]
else: else:
debug_transform = [] debug_transform = []
......
...@@ -1441,18 +1441,23 @@ class DebugTransform(CythonTransform): ...@@ -1441,18 +1441,23 @@ class DebugTransform(CythonTransform):
to enable debugging. to enable debugging.
""" """
def __init__(self, context): def __init__(self, context, options):
super(DebugTransform, self).__init__(context) super(DebugTransform, self).__init__(context)
self.visited = set() self.visited = set()
# our treebuilder and debug output writer # our treebuilder and debug output writer
# (see Cython.Debugger.debug_output.CythonDebugWriter) # (see Cython.Debugger.debug_output.CythonDebugWriter)
self.tb = self.context.debug_outputwriter 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): def visit_ModuleNode(self, node):
self.tb.module_name = node.full_module_name self.tb.module_name = node.full_module_name
attrs = dict( attrs = dict(
module_name=node.full_module_name, 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) self.tb.start('Module', attrs)
...@@ -1503,20 +1508,25 @@ class DebugTransform(CythonTransform): ...@@ -1503,20 +1508,25 @@ class DebugTransform(CythonTransform):
self.tb.end('Arguments') self.tb.end('Arguments')
self.tb.start('StepIntoFunctions') self.tb.start('StepIntoFunctions')
self.register_stepinto = True
self.visitchildren(node) self.visitchildren(node)
self.register_stepinto = False
self.tb.end('StepIntoFunctions') self.tb.end('StepIntoFunctions')
self.tb.end('Function') self.tb.end('Function')
return node return node
def visit_NameNode(self, node): def visit_NameNode(self, node):
if (node.type.is_cfunction and if (self.register_stepinto and node.type.is_cfunction and node.is_called):
node.is_called and # don't check node.entry.in_cinclude, as 'cdef extern: ...'
node.entry.in_cinclude): # 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) attrs = dict(name=node.entry.func_cname)
self.tb.start('StepIntoFunction', attrs=attrs) self.tb.start('StepIntoFunction', attrs=attrs)
self.tb.end('StepIntoFunction') self.tb.end('StepIntoFunction')
self.visitchildren(node) self.visitchildren(node)
return node return node
......
...@@ -20,25 +20,29 @@ import subprocess ...@@ -20,25 +20,29 @@ import subprocess
def usage(): def usage():
print("Usage: cygdb [PATH GDB_ARGUMENTS]") print("Usage: cygdb [PATH GDB_ARGUMENTS]")
def make_command_file(path_to_debug_info): def make_command_file(path_to_debug_info, prefix_code='', no_import=False):
debug_files = glob.glob( if not no_import:
os.path.join(path_to_debug_info, 'cython_debug/cython_debug_info_*')) pattern = os.path.join(path_to_debug_info,
'cython_debug/cython_debug_info_*')
if not debug_files: debug_files = glob.glob(pattern)
usage()
sys.exit('No debug files were found in %s. Aborting.' % ( if not debug_files:
os.path.abspath(path_to_debug_info))) usage()
sys.exit('No debug files were found in %s. Aborting.' % (
os.path.abspath(path_to_debug_info)))
fd, tempfilename = tempfile.mkstemp() fd, tempfilename = tempfile.mkstemp()
f = os.fdopen(fd, 'w') f = os.fdopen(fd, 'w')
f.write(prefix_code)
f.write('set breakpoint pending on\n') f.write('set breakpoint pending on\n')
f.write('python from Cython.Debugger import libcython\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() f.close()
return tempfilename 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 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 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): ...@@ -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 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) p = subprocess.Popen(['gdb', '-command', tempfilename] + gdb_argv)
while True: while True:
try: try:
......
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
...@@ -90,20 +90,43 @@ def require_cython_frame(function): ...@@ -90,20 +90,43 @@ def require_cython_frame(function):
'Cython function we know about.') 'Cython function we know about.')
return function(self, *args, **kwargs) return function(self, *args, **kwargs)
return wrapper 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 # Classes that represent the debug information
# Don't rename the parameters of these classes, they come directly from the XML # Don't rename the parameters of these classes, they come directly from the XML
class CythonModule(object): class CythonModule(object):
def __init__(self, module_name, filename): def __init__(self, module_name, filename, c_filename):
self.name = module_name self.name = module_name
self.filename = filename self.filename = filename
self.c_filename = c_filename
self.globals = {} self.globals = {}
# {cython_lineno: min(c_linenos)} # {cython_lineno: min(c_linenos)}
self.lineno_cy2c = {} self.lineno_cy2c = {}
# {c_lineno: cython_lineno} # {c_lineno: cython_lineno}
self.lineno_c2cy = {} self.lineno_c2cy = {}
self.functions = {}
def qualified_name(self, varname): def qualified_name(self, varname):
return '.'.join(self.name, varname) return '.'.join(self.name, varname)
...@@ -146,12 +169,15 @@ class CythonBase(object): ...@@ -146,12 +169,15 @@ class CythonBase(object):
@default_selected_gdb_frame(err=False) @default_selected_gdb_frame(err=False)
def is_python_function(self, frame): def is_python_function(self, frame):
return frame.name() == 'PyEval_EvalFrameEx' """
Tells if a frame is associated with a Python function.
@default_selected_gdb_frame() If we can't read the Python frame information, don't regard it as such.
def is_python_function(self, frame): """
return libpython.Frame(frame).is_evalframeex() 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() @default_selected_gdb_frame()
def get_c_function_name(self, frame): def get_c_function_name(self, frame):
return frame.name() return frame.name()
...@@ -170,9 +196,13 @@ class CythonBase(object): ...@@ -170,9 +196,13 @@ class CythonBase(object):
@default_selected_gdb_frame() @default_selected_gdb_frame()
def get_cython_lineno(self, 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) 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() @default_selected_gdb_frame()
def get_source_desc(self, frame): def get_source_desc(self, frame):
filename = lineno = lexer = None filename = lineno = lexer = None
...@@ -185,7 +215,7 @@ class CythonBase(object): ...@@ -185,7 +215,7 @@ class CythonBase(object):
pyframeobject = libpython.Frame(frame).get_pyop() pyframeobject = libpython.Frame(frame).get_pyop()
if not pyframeobject: 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() filename = pyframeobject.filename()
lineno = pyframeobject.current_line_num() lineno = pyframeobject.current_line_num()
...@@ -198,6 +228,25 @@ class CythonBase(object): ...@@ -198,6 +228,25 @@ class CythonBase(object):
def get_source_line(self, frame): def get_source_line(self, frame):
source_desc, lineno = self.get_source_desc() source_desc, lineno = self.get_source_desc()
return source_desc.get_source(lineno) 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): class SourceFileDescriptor(object):
def __init__(self, filename, lexer, formatter=None): def __init__(self, filename, lexer, formatter=None):
...@@ -238,13 +287,20 @@ class SourceFileDescriptor(object): ...@@ -238,13 +287,20 @@ class SourceFileDescriptor(object):
yield '%s %4d %s' % (prefix, start + idx, line) yield '%s %4d %s' % (prefix, start + idx, line)
def get_source(self, start, stop=None, lex_source=True, mark_line=0): 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: if not self.filename:
raise GdbError('Unable to retrieve source code') raise exc
if stop is None: if stop is None:
stop = start + 1 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 # Errors
...@@ -299,18 +355,24 @@ class CompleteUnqualifiedFunctionNames(CythonParameter): ...@@ -299,18 +355,24 @@ class CompleteUnqualifiedFunctionNames(CythonParameter):
class ColorizeSourceCode(CythonParameter): class ColorizeSourceCode(CythonParameter):
""" """
Tell cygdb whether to colorize source code Tell cygdb whether to colorize source code.
""" """
class TerminalBackground(CythonParameter): 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): class CythonParameters(object):
""" """
Simple container class that might get more functionality in the distant 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): def __init__(self):
...@@ -329,7 +391,12 @@ class CythonParameters(object): ...@@ -329,7 +391,12 @@ class CythonParameters(object):
gdb.COMMAND_FILES, gdb.COMMAND_FILES,
gdb.PARAM_STRING, gdb.PARAM_STRING,
"dark") "dark")
self.step_into_c_code = StepIntoCCode(
'cy_step_into_c_code',
gdb.COMMAND_RUNNING,
gdb.PARAM_BOOLEAN,
True)
parameters = CythonParameters() parameters = CythonParameters()
...@@ -340,6 +407,14 @@ class CythonCommand(gdb.Command, CythonBase): ...@@ -340,6 +407,14 @@ class CythonCommand(gdb.Command, CythonBase):
Base class for Cython commands 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): class CyCy(CythonCommand):
""" """
...@@ -348,6 +423,7 @@ class CyCy(CythonCommand): ...@@ -348,6 +423,7 @@ class CyCy(CythonCommand):
cy import cy import
cy break cy break
cy step cy step
cy next
cy print cy print
cy list cy list
cy locals cy locals
...@@ -357,31 +433,29 @@ class CyCy(CythonCommand): ...@@ -357,31 +433,29 @@ class CyCy(CythonCommand):
cy down cy down
""" """
def __init__(self): name = 'cy'
super(CythonCommand, self).__init__( command_class = gdb.COMMAND_NONE
'cy', gdb.COMMAND_NONE, gdb.COMPLETE_COMMAND, prefix=True) completer_class = gdb.COMPLETE_COMMAND
self.import_ = CyImport( def __init__(self, *args):
'cy import', gdb.COMMAND_STATUS, gdb.COMPLETE_FILENAME) super(CythonCommand, self).__init__(*args, prefix=True)
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)
self.locals = CyLocals( commands = dict(
'cy locals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE) import_ = CyImport.register(),
self.globals = CyGlobals( break_ = CyBreak.register(),
'cy globals', gdb.COMMAND_STACK, gdb.COMPLETE_NONE) 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') for command_name, command in commands.iteritems():
command.cy = self
objs = (self.import_, self.break_, self.step, self.list, self.print_, setattr(self, command_name, command)
self.locals, self.globals, self.cy_cname)
for obj in objs:
obj.cy = self
# Cython module namespace # Cython module namespace
self.cython_namespace = {} self.cython_namespace = {}
...@@ -403,15 +477,19 @@ class CyImport(CythonCommand): ...@@ -403,15 +477,19 @@ class CyImport(CythonCommand):
Import debug information outputted by the Cython compiler Import debug information outputted by the Cython compiler
Example: cy import FILE... Example: cy import FILE...
""" """
name = 'cy import'
command_class = gdb.COMMAND_STATUS
completer_class = gdb.COMPLETE_FILENAME
def invoke(self, args, from_tty): def invoke(self, args, from_tty):
args = args.encode(_filesystemencoding) args = args.encode(_filesystemencoding)
for arg in string_to_argv(args): for arg in string_to_argv(args):
try: try:
f = open(arg) f = open(arg)
except OSError, e: except OSError, e:
print('Unable to open file %r: %s' % (args, e.args[1])) raise gdb.GdbError('Unable to open file %r: %s' %
return (args, e.args[1]))
t = etree.parse(f) t = etree.parse(f)
...@@ -426,15 +504,20 @@ class CyImport(CythonCommand): ...@@ -426,15 +504,20 @@ class CyImport(CythonCommand):
for function in module.find('Functions'): for function in module.find('Functions'):
cython_function = CythonFunction(module=cython_module, cython_function = CythonFunction(module=cython_module,
**function.attrib) **function.attrib)
# update the global function mappings # update the global function mappings
self.cy.functions_by_name[cython_function.name].append( name = cython_function.name
cython_function)
self.cy.functions_by_name[name].append(cython_function)
self.cy.functions_by_qualified_name[ self.cy.functions_by_qualified_name[
cython_function.qualified_name] = cython_function cython_function.qualified_name] = cython_function
self.cy.functions_by_cname[ self.cy.functions_by_cname[
cython_function.cname] = cython_function 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'): for local in function.find('Locals'):
d = local.attrib d = local.attrib
cython_function.locals[d['name']] = CythonVariable(**d) cython_function.locals[d['name']] = CythonVariable(**d)
...@@ -469,13 +552,16 @@ class CyBreak(CythonCommand): ...@@ -469,13 +552,16 @@ class CyBreak(CythonCommand):
cy break cython_module:lineno... cy break cython_module:lineno...
""" """
name = 'cy break'
command_class = gdb.COMMAND_BREAKPOINTS
def _break_pyx(self, name): def _break_pyx(self, name):
modulename, _, lineno = name.partition(':') modulename, _, lineno = name.partition(':')
lineno = int(lineno) lineno = int(lineno)
cython_module = self.cy.cython_namespace[modulename] cython_module = self.cy.cython_namespace[modulename]
if lineno in cython_module.lineno_cy2c: if lineno in cython_module.lineno_cy2c:
c_lineno = cython_module.lineno_cy2c[lineno] 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) gdb.execute('break ' + breakpoint)
else: else:
raise GdbError("Not a valid line number. " raise GdbError("Not a valid line number. "
...@@ -554,78 +640,74 @@ class CyBreak(CythonCommand): ...@@ -554,78 +640,74 @@ class CyBreak(CythonCommand):
return compl 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): def lineno(self, frame):
self.cython_func = self.get_cython_function() # Take care of the Python and Cython levels. We need to care for both
self.beginline = self.get_cython_lineno() # as we can't simply dispath to 'py-step', since that would work for
self.curframe = gdb.selected_frame() # stepping through Python code, but it would not step back into Cython-
# related code. The C level should be dispatched to the 'step' command.
def next_step(self, command): if self.is_cython_function(frame):
"returns whether to continue stepping" return self.get_cython_lineno(frame)
result = gdb.execute(command, to_string=True) else:
newframe = gdb.selected_frame() 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') return result.lstrip()
c2 = (newframe == self.curframe and
self.get_cython_lineno() > self.beginline)
return not c1 and not c2
def end_stepping(self): @classmethod
sys.stdout.write(self.get_source_line()) 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): class CyNext(CythonCodeStepper):
for nthstep in xrange(nsteps): "Step-over Python code."
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)
name = 'cy next'
stepper = False
class CyNext(CythonCommand, CodeStepperMixin): @dispatch_on_frame(c_command='next')
def invoke(self, *args, **kwargs):
def next(self, nsteps=1): super(CythonCodeStepper, self).invoke(*args, **kwargs)
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)
class CyList(CythonCommand): 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): def invoke(self, _, from_tty):
sd, lineno = self.get_source_desc() sd, lineno = self.get_source_desc()
source = sd.get_source(lineno - 5, lineno + 5, mark_line=lineno) source = sd.get_source(lineno - 5, lineno + 5, mark_line=lineno)
...@@ -637,27 +719,34 @@ class CyPrint(CythonCommand): ...@@ -637,27 +719,34 @@ class CyPrint(CythonCommand):
Print a Cython variable using 'cy-print x' or 'cy-print module.function.x' 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): def invoke(self, name, from_tty):
try: gdb.execute('print ' + self.cy.cy_cname.invoke(name, string=True))
cname = cy.cy_cname.invoke(name)
except gdb.GdbError:
cname = name
gdb.execute('print ' + cname)
def complete(self): def complete(self):
if self.is_cython_function(): if self.is_cython_function():
f = self.get_cython_function() f = self.get_cython_function()
return list(itertools.chain(f.locals, f.globals)) return list(itertools.chain(f.locals, f.globals))
return [] else:
return []
class CyLocals(CythonCommand): 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): def ns(self):
return self.get_cython_function().locals 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): def invoke(self, name, from_tty):
try: try:
ns = self.ns() ns = self.ns()
...@@ -682,10 +771,18 @@ class CyLocals(CythonCommand): ...@@ -682,10 +771,18 @@ class CyLocals(CythonCommand):
class CyGlobals(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): def ns(self):
return self.get_cython_function().globals 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): def invoke(self, name, from_tty):
# include globals from the debug info XML file! # include globals from the debug info XML file!
m = gdb.parse_and_eval('__pyx_m') m = gdb.parse_and_eval('__pyx_m')
...@@ -709,26 +806,54 @@ class CyGlobals(CythonCommand): ...@@ -709,26 +806,54 @@ class CyGlobals(CythonCommand):
class CyCName(gdb.Function, CythonBase): 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 @require_cython_frame
def invoke(self, cyname, frame=None): def invoke(self, cyname, string=False, frame=None):
frame = frame or gdb.selected_frame() frame = frame or gdb.selected_frame()
cname = None 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): if self.is_cython_function(frame):
cython_function = self.get_cython_function(frame) cython_function = self.get_cython_function(frame)
if cyname in cython_function.locals: if cyname in cython_function.locals:
cname = cython_function.locals[cyname].cname cname = cython_function.locals[cyname].cname
elif cyname in cython_function.module.globals: elif cyname in cython_function.module.globals:
cname = cython_function.module.globals[cyname].cname 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: if not cname:
raise gdb.GdbError('No such Cython variable: %s' % cyname) 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() cy = CyCy.register()
\ No newline at end of file \ No newline at end of file
...@@ -44,8 +44,12 @@ the type names are known to the debugger ...@@ -44,8 +44,12 @@ the type names are known to the debugger
The module also extends gdb with some python-specific commands. The module also extends gdb with some python-specific commands.
''' '''
from __future__ import with_statement from __future__ import with_statement
import gdb
import os
import sys import sys
import tempfile
import gdb
# Look up the gdb.Type for some standard types: # Look up the gdb.Type for some standard types:
_type_char_ptr = gdb.lookup_type('char').pointer() # char* _type_char_ptr = gdb.lookup_type('char').pointer() # char*
...@@ -1448,3 +1452,165 @@ class PyLocals(gdb.Command): ...@@ -1448,3 +1452,165 @@ class PyLocals(gdb.Command):
pyop_value.get_truncated_repr(MAX_OUTPUT_LEN))) pyop_value.get_truncated_repr(MAX_OUTPUT_LEN)))
PyLocals() 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
...@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb ...@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1: if len(sys.argv) > 1:
cygdb.main(path_to_debug_info=sys.argv[1], path_to_debug_info = sys.argv[1]
gdb_argv=sys.argv[2:])
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: else:
cygdb.main() cygdb.main()
...@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb ...@@ -6,7 +6,14 @@ from Cython.Debugger import Cygdb as cygdb
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) > 1: if len(sys.argv) > 1:
cygdb.main(path_to_debug_info=sys.argv[1], path_to_debug_info = sys.argv[1]
gdb_argv=sys.argv[2:])
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: else:
cygdb.main() cygdb.main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment