Skip to content

Commit 2b7ff35

Browse files
committed
fix: Route __repr__ and get_undecorated_callback behavior with __call__.
This fixes two issues: - The Route.get_undecorated_callback() method failed to detect objects with a `__call__` method as callable. It also did not follow `__wrapped__` if present. Both works now. - The Route.__repr__ method would fail if the callable does not have a `__name__` which can happen with `__call__`-able objects. fix: #1488
1 parent 131f397 commit 2b7ff35

File tree

2 files changed

+56
-13
lines changed

2 files changed

+56
-13
lines changed

bottle.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -573,18 +573,17 @@ def get_undecorated_callback(self):
573573
""" Return the callback. If the callback is a decorated function, try to
574574
recover the original function. """
575575
func = self.callback
576-
func = getattr(func, '__func__' if py3k else 'im_func', func)
577-
closure_attr = '__closure__' if py3k else 'func_closure'
578-
while hasattr(func, closure_attr) and getattr(func, closure_attr):
579-
attributes = getattr(func, closure_attr)
580-
func = attributes[0].cell_contents
581-
582-
# in case of decorators with multiple arguments
583-
if not isinstance(func, FunctionType):
584-
# pick first FunctionType instance from multiple arguments
585-
func = filter(lambda x: isinstance(x, FunctionType),
586-
map(lambda x: x.cell_contents, attributes))
587-
func = list(func)[0] # py3 support
576+
while True:
577+
if getattr(func, '__wrapped__', False):
578+
func = func.__wrapped__
579+
elif getattr(func, '__func__', False):
580+
func = func.__func__
581+
elif getattr(func, '__closure__', False):
582+
cells_values = (cell.cell_contents for cell in func.__closure__)
583+
isfunc = lambda x: isinstance(x, FunctionType) or hasattr(x, '__call__')
584+
func = next(iter(filter(isfunc, cells_values)), func)
585+
else:
586+
break
588587
return func
589588

590589
def get_callback_args(self):
@@ -603,7 +602,9 @@ def get_config(self, key, default=None):
603602

604603
def __repr__(self):
605604
cb = self.get_undecorated_callback()
606-
return '<%s %s -> %s:%s>' % (self.method, self.rule, cb.__module__, cb.__name__)
605+
return '<%s %s -> %s:%s>' % (
606+
self.method, self.rule, cb.__module__, getattr(cb, '__name__', '__call__')
607+
)
607608

608609
###############################################################################
609610
# Application Object ###########################################################

test/test_route.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,45 @@ def test_callback_inspection_newsig(self):
6565
eval(compile('def foo(a, *, b=5): pass', '<foo>', 'exec'), env, env)
6666
route = bottle.Route(bottle.Bottle(), None, None, env['foo'])
6767
self.assertEqual(set(route.get_callback_args()), set(['a', 'b']))
68+
69+
def test_unwrap_wrapped(self):
70+
import functools
71+
def func(): pass
72+
@functools.wraps(func)
73+
def wrapped():
74+
return func()
75+
76+
route = bottle.Route(bottle.Bottle(), None, None, wrapped)
77+
self.assertEqual(route.get_undecorated_callback(), func)
78+
79+
def test_unwrap_closure(self):
80+
def func(): pass
81+
wrapped = _null_decorator(func)
82+
route = bottle.Route(bottle.Bottle(), None, None, wrapped)
83+
self.assertEqual(route.get_undecorated_callback(), func)
84+
85+
def test_unwrap_callable(self):
86+
class Foo:
87+
def __call__(self): pass
88+
89+
func = Foo()
90+
wrapped = _null_decorator(func)
91+
route = bottle.Route(bottle.Bottle(), None, None, wrapped)
92+
self.assertEqual(route.get_undecorated_callback(), func)
93+
repr(route) # Raised cause cb has no '__name__'
94+
95+
def test_unwrap_method(self):
96+
def func(self): pass
97+
98+
class Foo:
99+
test = _null_decorator(func)
100+
101+
wrapped = Foo().test
102+
route = bottle.Route(bottle.Bottle(), None, None, wrapped)
103+
self.assertEqual(route.get_undecorated_callback(), func)
104+
105+
106+
def _null_decorator(func):
107+
def wrapper():
108+
return func()
109+
return wrapper

0 commit comments

Comments
 (0)