Commit 7d98e4a1 authored by Mark Florisson's avatar Mark Florisson

Debugger: Fix closure support + tests

Debugger: Recognition of module-level Cython code (initmodulename and PyInit_modulename)
Fix debug flag (import Parsing when needed)
parent e5bb833d
...@@ -18,7 +18,11 @@ from time import time ...@@ -18,7 +18,11 @@ from time import time
import Code import Code
import Errors import Errors
import Parsing # Do not import Parsing here, import it when needed, because Parsing imports
# Nodes, which globally needs debug command line options initialized to set a
# conditional metaclass. These options are processed by CmdLine called from
# main() in this file.
# import Parsing
import Version import Version
from Scanning import PyrexScanner, FileSourceDescriptor from Scanning import PyrexScanner, FileSourceDescriptor
from Errors import PyrexError, CompileError, InternalError, AbortError, error, warning from Errors import PyrexError, CompileError, InternalError, AbortError, error, warning
...@@ -493,6 +497,7 @@ class Context(object): ...@@ -493,6 +497,7 @@ class Context(object):
try: try:
f = Utils.open_source_file(source_filename, "rU") f = Utils.open_source_file(source_filename, "rU")
try: try:
import Parsing
s = PyrexScanner(f, source_desc, source_encoding = f.encoding, s = PyrexScanner(f, source_desc, source_encoding = f.encoding,
scope = scope, context = self) scope = scope, context = self)
tree = Parsing.p_module(s, pxd, full_module_name) tree = Parsing.p_module(s, pxd, full_module_name)
......
...@@ -1591,6 +1591,10 @@ class DebugTransform(CythonTransform): ...@@ -1591,6 +1591,10 @@ class DebugTransform(CythonTransform):
#self.c_output_file = options.output_file #self.c_output_file = options.output_file
self.c_output_file = result.c_file self.c_output_file = result.c_file
# Closure support, basically treat nested functions as if the AST were
# never nested
self.nested_funcdefs = []
# tells visit_NameNode whether it should register step-into functions # tells visit_NameNode whether it should register step-into functions
self.register_stepinto = False self.register_stepinto = False
...@@ -1605,7 +1609,17 @@ class DebugTransform(CythonTransform): ...@@ -1605,7 +1609,17 @@ class DebugTransform(CythonTransform):
# serialize functions # serialize functions
self.tb.start('Functions') self.tb.start('Functions')
# First, serialize functions normally...
self.visitchildren(node) self.visitchildren(node)
# ... then, serialize nested functions
for nested_funcdef in self.nested_funcdefs:
self.visit_FuncDefNode(nested_funcdef)
self.register_stepinto = True
self.serialize_modulenode_as_function(node)
self.register_stepinto = False
self.tb.end('Functions') self.tb.end('Functions')
# 2.3 compatibility. Serialize global variables # 2.3 compatibility. Serialize global variables
...@@ -1625,8 +1639,16 @@ class DebugTransform(CythonTransform): ...@@ -1625,8 +1639,16 @@ class DebugTransform(CythonTransform):
# Cython.Compiler.ModuleNode.ModuleNode._serialize_lineno_map # Cython.Compiler.ModuleNode.ModuleNode._serialize_lineno_map
return node return node
def visit_FuncDefNode(self, node): def visit_FuncDefNode(self, node):
self.visited.add(node.local_scope.qualified_name) self.visited.add(node.local_scope.qualified_name)
if getattr(node, 'is_wrapper', False):
return node
if self.register_stepinto:
self.nested_funcdefs.append(node)
return node
# node.entry.visibility = 'extern' # node.entry.visibility = 'extern'
if node.py_func is None: if node.py_func is None:
pf_cname = '' pf_cname = ''
...@@ -1678,6 +1700,51 @@ class DebugTransform(CythonTransform): ...@@ -1678,6 +1700,51 @@ class DebugTransform(CythonTransform):
self.visitchildren(node) self.visitchildren(node)
return node return node
def serialize_modulenode_as_function(self, node):
"""
Serialize the module-level code as a function so the debugger will know
it's a "relevant frame" and it will know where to set the breakpoint
for 'break modulename'.
"""
name = node.full_module_name.rpartition('.')[-1]
cname_py2 = 'init' + name
cname_py3 = 'PyInit_' + name
py2_attrs = dict(
name=name,
cname=cname_py2,
pf_cname='',
# Ignore the qualified_name, breakpoints should be set using
# `cy break modulename:lineno` for module-level breakpoints.
qualified_name='',
lineno='1',
is_initmodule_function="True",
)
py3_attrs = dict(py2_attrs, cname=cname_py3)
self._serialize_modulenode_as_function(node, py2_attrs)
self._serialize_modulenode_as_function(node, py3_attrs)
def _serialize_modulenode_as_function(self, node, attrs):
self.tb.start('Function', attrs=attrs)
self.tb.start('Locals')
self.serialize_local_variables(node.scope.entries)
self.tb.end('Locals')
self.tb.start('Arguments')
self.tb.end('Arguments')
self.tb.start('StepIntoFunctions')
self.register_stepinto = True
self.visitchildren(node)
self.register_stepinto = False
self.tb.end('StepIntoFunctions')
self.tb.end('Function')
def serialize_local_variables(self, entries): def serialize_local_variables(self, entries):
for entry in entries.values(): for entry in entries.values():
if entry.type.is_pyobject: if entry.type.is_pyobject:
...@@ -1685,10 +1752,19 @@ class DebugTransform(CythonTransform): ...@@ -1685,10 +1752,19 @@ class DebugTransform(CythonTransform):
else: else:
vartype = 'CObject' vartype = 'CObject'
cname = entry.cname if entry.from_closure:
# if entry.type.is_extension_type: # We're dealing with a closure where a variable from an outer
# cname = entry.type.typeptr_cname # scope is accessed, get it from the scope object.
cname = '%s->%s' % (Naming.cur_scope_cname,
entry.outer_entry.cname)
qname = '%s.%s.%s' % (entry.scope.outer_scope.qualified_name,
entry.scope.name,
entry.name)
else:
cname = entry.cname
qname = entry.qualified_name
if not entry.pos: if not entry.pos:
# this happens for variables that are not in the user's code, # this happens for variables that are not in the user's code,
# e.g. for the global __builtins__, __doc__, etc. We can just # e.g. for the global __builtins__, __doc__, etc. We can just
...@@ -1696,11 +1772,11 @@ class DebugTransform(CythonTransform): ...@@ -1696,11 +1772,11 @@ class DebugTransform(CythonTransform):
lineno = '0' lineno = '0'
else: else:
lineno = str(entry.pos[1]) lineno = str(entry.pos[1])
attrs = dict( attrs = dict(
name=entry.name, name=entry.name,
cname=cname, cname=cname,
qualified_name=entry.qualified_name, qualified_name=qname,
type=vartype, type=vartype,
lineno=lineno) lineno=lineno)
......
...@@ -182,7 +182,8 @@ class TestDebugTransform(DebuggerTestCase): ...@@ -182,7 +182,8 @@ class TestDebugTransform(DebuggerTestCase):
self.assertEqual('PythonObject', xml_globals.get('python_var')) self.assertEqual('PythonObject', xml_globals.get('python_var'))
# test functions # test functions
funcnames = 'codefile.spam', 'codefile.ham', 'codefile.eggs' funcnames = ('codefile.spam', 'codefile.ham', 'codefile.eggs',
'codefile.closure', 'codefile.inner')
required_xml_attrs = 'name', 'cname', 'qualified_name' required_xml_attrs = 'name', 'cname', 'qualified_name'
assert all([f in xml_funcs for f in funcnames]) assert all([f in xml_funcs for f in funcnames])
spam, ham, eggs = [xml_funcs[funcname] for funcname in funcnames] spam, ham, eggs = [xml_funcs[funcname] for funcname in funcnames]
......
...@@ -21,16 +21,34 @@ def spam(a=0): ...@@ -21,16 +21,34 @@ def spam(a=0):
puts("spam") puts("spam")
os.path.join("foo", "bar") os.path.join("foo", "bar")
some_c_function() some_c_function()
cpdef eggs():
pass
cdef ham(): cdef ham():
pass pass
cpdef eggs():
pass
cdef class SomeClass(object): cdef class SomeClass(object):
def spam(self): def spam(self):
pass pass
def closure():
a = 1
def inner():
b = 2
# access closed over variables
print a, b
return inner
def closure_without_closing_variables():
a = 1
def inner2():
b = 2
print b
return inner2
closure()()
closure_without_closing_variables()()
spam() spam()
print "bye!" print "bye!"
...@@ -98,7 +98,7 @@ class TestDebugInformationClasses(DebugTestCase): ...@@ -98,7 +98,7 @@ class TestDebugInformationClasses(DebugTestCase):
'codefile.SomeClass.spam') 'codefile.SomeClass.spam')
self.assertEqual(self.spam_func.module, self.module) self.assertEqual(self.spam_func.module, self.module)
assert self.eggs_func.pf_cname assert self.eggs_func.pf_cname, (self.eggs_func, self.eggs_func.pf_cname)
assert not self.ham_func.pf_cname assert not self.ham_func.pf_cname
assert not self.spam_func.pf_cname assert not self.spam_func.pf_cname
assert not self.spam_meth.pf_cname assert not self.spam_meth.pf_cname
...@@ -135,11 +135,21 @@ class TestBreak(DebugTestCase): ...@@ -135,11 +135,21 @@ class TestBreak(DebugTestCase):
self.assertEqual(bp.type, gdb.BP_BREAKPOINT) self.assertEqual(bp.type, gdb.BP_BREAKPOINT)
assert self.spam_func.cname in bp.location assert self.spam_func.cname in bp.location
assert bp.enabled assert bp.enabled
def test_python_break(self): def test_python_break(self):
gdb.execute('cy break -p join') gdb.execute('cy break -p join')
assert 'def join(' in gdb.execute('cy run', to_string=True) assert 'def join(' in gdb.execute('cy run', to_string=True)
def test_break_lineno(self):
beginline = 'import os'
nextline = 'cdef int c_var = 12'
self.break_and_run(beginline)
self.lineno_equals(beginline)
step_result = gdb.execute('cy step', to_string=True)
self.lineno_equals(nextline)
assert step_result.rstrip().endswith(nextline)
class TestKilled(DebugTestCase): class TestKilled(DebugTestCase):
...@@ -341,6 +351,19 @@ class TestExec(DebugTestCase): ...@@ -341,6 +351,19 @@ class TestExec(DebugTestCase):
gdb.execute('cy exec some_random_var = 14') gdb.execute('cy exec some_random_var = 14')
self.assertEqual('14', self.eval_command('some_random_var')) self.assertEqual('14', self.eval_command('some_random_var'))
class TestClosure(DebugTestCase):
def test_cython_closure(self):
self.break_and_run('def inner():')
self.assertEqual(str(self.read_var('a')), '1')
print_result = gdb.execute('cy print a', to_string=True).strip()
self.assertEqual(print_result, 'a = 1')
def test_cython_closure_no_closing_variables(self):
self.break_and_run('def inner2():')
self.assertEqual(gdb.execute('cy locals', to_string=True), '')
_do_debug = os.environ.get('GDB_DEBUG') _do_debug = os.environ.get('GDB_DEBUG')
if _do_debug: if _do_debug:
...@@ -381,7 +404,7 @@ def runtests(): ...@@ -381,7 +404,7 @@ def runtests():
returned to the parent test process. returned to the parent test process.
""" """
from Cython.Debugger.Tests import test_libpython_in_gdb from Cython.Debugger.Tests import test_libpython_in_gdb
success_libcython = run_unittest_in_module(__name__) success_libcython = run_unittest_in_module(__name__)
success_libpython = run_unittest_in_module(test_libpython_in_gdb.__name__) success_libpython = run_unittest_in_module(test_libpython_in_gdb.__name__)
......
...@@ -154,9 +154,6 @@ class CythonModule(object): ...@@ -154,9 +154,6 @@ class CythonModule(object):
self.lineno_c2cy = {} self.lineno_c2cy = {}
self.functions = {} self.functions = {}
def qualified_name(self, varname):
return '.'.join(self.name, varname)
class CythonVariable(object): class CythonVariable(object):
def __init__(self, name, cname, qualified_name, type, lineno): def __init__(self, name, cname, qualified_name, type, lineno):
...@@ -174,7 +171,8 @@ class CythonFunction(CythonVariable): ...@@ -174,7 +171,8 @@ class CythonFunction(CythonVariable):
pf_cname, pf_cname,
qualified_name, qualified_name,
lineno, lineno,
type=CObject): type=CObject,
is_initmodule_function="False"):
super(CythonFunction, self).__init__(name, super(CythonFunction, self).__init__(name,
cname, cname,
qualified_name, qualified_name,
...@@ -182,6 +180,7 @@ class CythonFunction(CythonVariable): ...@@ -182,6 +180,7 @@ class CythonFunction(CythonVariable):
lineno) lineno)
self.module = module self.module = module
self.pf_cname = pf_cname self.pf_cname = pf_cname
self.is_initmodule_function = is_initmodule_function == "True"
self.locals = {} self.locals = {}
self.arguments = [] self.arguments = []
self.step_into_functions = set() self.step_into_functions = set()
...@@ -243,7 +242,8 @@ class CythonBase(object): ...@@ -243,7 +242,8 @@ class CythonBase(object):
pyframeobject = libpython.Frame(frame).get_pyop() pyframeobject = libpython.Frame(frame).get_pyop()
if not pyframeobject: if not pyframeobject:
raise gdb.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()
...@@ -752,14 +752,20 @@ class CyBreak(CythonCommand): ...@@ -752,14 +752,20 @@ class CyBreak(CythonCommand):
def _break_funcname(self, funcname): def _break_funcname(self, funcname):
func = self.cy.functions_by_qualified_name.get(funcname) func = self.cy.functions_by_qualified_name.get(funcname)
if func and func.is_initmodule_function:
func = None
break_funcs = [func] break_funcs = [func]
if not func: if not func:
funcs = self.cy.functions_by_name.get(funcname) funcs = self.cy.functions_by_name.get(funcname) or []
funcs = [f for f in funcs if not f.is_initmodule_function]
if not funcs: if not funcs:
gdb.execute('break ' + funcname) gdb.execute('break ' + funcname)
return return
if len(funcs) > 1: if len(funcs) > 1:
# multiple functions, let the user pick one # multiple functions, let the user pick one
print 'There are multiple such functions:' print 'There are multiple such functions:'
...@@ -811,18 +817,28 @@ class CyBreak(CythonCommand): ...@@ -811,18 +817,28 @@ class CyBreak(CythonCommand):
@dont_suppress_errors @dont_suppress_errors
def complete(self, text, word): def complete(self, text, word):
names = self.cy.functions_by_qualified_name # Filter init-module functions (breakpoints can be set using
# modulename:linenumber).
names = [n for n, L in self.cy.functions_by_name.iteritems()
if any(not f.is_initmodule_function for f in L)]
qnames = [n for n, f in self.cy.functions_by_qualified_name.iteritems()
if not f.is_initmodule_function]
if parameters.complete_unqualified: if parameters.complete_unqualified:
names = itertools.chain(names, self.cy.functions_by_name) all_names = itertools.chain(qnames, names)
else:
all_names = qnames
words = text.strip().split() words = text.strip().split()
if words and '.' in words[-1]: if not words or '.' not in words[-1]:
lastword = words[-1] # complete unqualified
compl = [n for n in self.cy.functions_by_qualified_name
if n.startswith(lastword)]
else:
seen = set(text[:-len(word)].split()) seen = set(text[:-len(word)].split())
return [n for n in names if n.startswith(word) and n not in seen] return [n for n in all_names
if n.startswith(word) and n not in seen]
# complete qualified name
lastword = words[-1]
compl = [n for n in qnames if n.startswith(lastword)]
if len(lastword) > len(word): if len(lastword) > len(word):
# readline sees something (e.g. a '.') as a word boundary, so don't # readline sees something (e.g. a '.') as a word boundary, so don't
...@@ -862,6 +878,7 @@ class CythonInfo(CythonBase, libpython.PythonInfo): ...@@ -862,6 +878,7 @@ class CythonInfo(CythonBase, libpython.PythonInfo):
def runtime_break_functions(self): def runtime_break_functions(self):
if self.is_cython_function(): if self.is_cython_function():
return self.get_cython_function().step_into_functions return self.get_cython_function().step_into_functions
return ()
def static_break_functions(self): def static_break_functions(self):
result = ['PyEval_EvalFrameEx'] result = ['PyEval_EvalFrameEx']
...@@ -1091,7 +1108,13 @@ class CyLocals(CythonCommand): ...@@ -1091,7 +1108,13 @@ class CyLocals(CythonCommand):
@dispatch_on_frame(c_command='info locals', python_command='py-locals') @dispatch_on_frame(c_command='info locals', python_command='py-locals')
def invoke(self, args, from_tty): def invoke(self, args, from_tty):
local_cython_vars = self.get_cython_function().locals cython_function = self.get_cython_function()
if cython_function.is_initmodule_function:
self.cy.globals.invoke(args, from_tty)
return
local_cython_vars = cython_function.locals
max_name_length = len(max(local_cython_vars, key=len)) max_name_length = len(max(local_cython_vars, key=len))
for name, cyvar in sorted(local_cython_vars.iteritems(), key=sortkey): for name, cyvar in sorted(local_cython_vars.iteritems(), key=sortkey):
if self.is_initialized(self.get_cython_function(), cyvar.name): if self.is_initialized(self.get_cython_function(), cyvar.name):
......
...@@ -1906,8 +1906,6 @@ class ExecutionControlCommandBase(gdb.Command): ...@@ -1906,8 +1906,6 @@ class ExecutionControlCommandBase(gdb.Command):
for location in all_locations: for location in all_locations:
result = gdb.execute('break %s' % location, to_string=True) result = gdb.execute('break %s' % location, to_string=True)
yield re.search(r'Breakpoint (\d+)', result).group(1) yield re.search(r'Breakpoint (\d+)', result).group(1)
def delete_breakpoints(self, breakpoint_list): def delete_breakpoints(self, breakpoint_list):
for bp in breakpoint_list: for bp in breakpoint_list:
......
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