diff --git a/pecan/tests/test_util.py b/pecan/tests/test_util.py index 00e81f5..903d084 100644 --- a/pecan/tests/test_util.py +++ b/pecan/tests/test_util.py @@ -93,3 +93,23 @@ class TestArgSpec(unittest.TestCase): actual = util.getargspec(dec(True)( self.controller.static_index)) assert expected == actual + + def test_nested_cells(self): + + def before(handler): + def deco(f): + def wrapped(*args, **kwargs): + if callable(handler): + handler() + return f(*args, **kwargs) + return wrapped + return deco + + class RootController(object): + @expose() + @before(lambda: True) + def index(self, a, b, c): + return 'Hello, World!' + + argspec = util._cfg(RootController.index)['argspec'] + assert argspec.args == ['self', 'a', 'b', 'c'] diff --git a/pecan/util.py b/pecan/util.py index bdb5d2b..2c61475 100644 --- a/pecan/util.py +++ b/pecan/util.py @@ -26,15 +26,26 @@ def getargspec(method): # NOTE(sileht): if the closure is None we cannot look deeper, # so return actual argspec, this occurs when the method # is static for example. - if func_closure is None: + if not func_closure: return argspec - closure = next( - ( - c for c in func_closure if six.callable(c.cell_contents) - ), - None + closure = None + # In the case of deeply nested decorators (with arguments), it's possible + # that there are several callables in scope; Take a best guess and go + # with the one that looks most like a pecan controller function + # ('self' is the first argument) + func_closure = filter( + lambda c: six.callable(c.cell_contents), + func_closure ) + func_closure = sorted( + func_closure, + key=lambda c: 'self' in c.cell_contents.__code__.co_varnames, + reverse=True + ) + + closure = func_closure[0] + method = closure.cell_contents return getargspec(method)