Add a decorator decorator that checks func args

This patch adds a decorator for decorators that allows specifying the
arg names that the function being decorated must accept. If the function
being decorated does not accept the specified args, an exception will be
thrown during 'decoration', meaning import time usually.

If however the function accepts variable number of arguments, the
decorator will be a no-op as it is not possible to guess what will be
passed at runtime. This is however good enough for Nova's compute
manager decorators that inspired this work, as they decorate methods
with well defined and even versioned arguments.

Several decorators used in compute manager are now fortified with this
"compile time" check.

Partial-bug:#1296593
Change-Id: I00ebcc8a092fa0894ba8764699093f59a46b8647
This commit is contained in:
Nikola Dipanov 2014-03-07 15:04:44 +01:00
parent 793f517f5d
commit 7cf7d4a6db
3 changed files with 71 additions and 0 deletions

View File

@ -225,6 +225,7 @@ wrap_exception = functools.partial(exception.wrap_exception,
get_notifier=get_notifier)
@utils.expects_func_args('migration')
def errors_out_migration(function):
"""Decorator to error out migration on failure."""
@ -255,6 +256,7 @@ def errors_out_migration(function):
return decorated_function
@utils.expects_func_args('instance')
def reverts_task_state(function):
"""Decorator to revert task_state on failure."""
@ -308,6 +310,7 @@ def wrap_instance_fault(function):
return decorated_function
@utils.expects_func_args('instance')
def wrap_instance_event(function):
"""Wraps a method to log the event taken on the instance, and result.
@ -331,6 +334,7 @@ def wrap_instance_event(function):
return decorated_function
@utils.expects_func_args('image_id', 'instance')
def delete_image_on_error(function):
"""Used for snapshot related method to ensure the image created in
compute.api is deleted when an error occurs.

View File

@ -709,6 +709,52 @@ class WrappedCodeTestCase(test.NoDBTestCase):
self.assertIn('blue', func_code.co_varnames)
class ExpectedArgsTestCase(test.NoDBTestCase):
def test_passes(self):
@utils.expects_func_args('foo', 'baz')
def dec(f):
return f
@dec
def func(foo, bar, baz="lol"):
pass
def test_raises(self):
@utils.expects_func_args('foo', 'baz')
def dec(f):
return f
def func(bar, baz):
pass
self.assertRaises(TypeError, dec, func)
def test_var_no_of_args(self):
@utils.expects_func_args('foo')
def dec(f):
return f
@dec
def func(bar, *args, **kwargs):
pass
def test_more_layers(self):
@utils.expects_func_args('foo', 'baz')
def dec(f):
return f
def dec_2(f):
def inner_f(*a, **k):
return f()
return inner_f
@dec_2
def func(bar, baz):
pass
self.assertRaises(TypeError, dec, func)
class StringLengthTestCase(test.NoDBTestCase):
def test_check_string_length(self):
self.assertIsNone(utils.check_string_length(

View File

@ -914,6 +914,27 @@ def get_wrapped_function(function):
return _get_wrapped_function(function)
def expects_func_args(*args):
def _decorator_checker(dec):
@functools.wraps(dec)
def _decorator(f):
base_f = get_wrapped_function(f)
arg_names, a, kw, _default = inspect.getargspec(base_f)
if a or kw or set(args) <= set(arg_names):
# NOTE (ndipanov): We can't really tell if correct stuff will
# be passed if it's a function with *args or **kwargs so
# we still carry on and hope for the best
return dec(f)
else:
raise TypeError("Decorated function %(f_name)s does not "
"have the arguments expected by the "
"decorator %(d_name)s" %
{'f_name': base_f.__name__,
'd_name': dec.__name__})
return _decorator
return _decorator_checker
class ExceptionHelper(object):
"""Class to wrap another and translate the ClientExceptions raised by its
function calls to the actual ones.