def __init__(self, *args, **kwargs): ''' Constructor. It accepts arbitrary arguments and uses them to build "argument specification" which will be used when calling decorated function. ''' self.arg_spec = ArgumentSpec() for i, arg in enumerate(args): self.arg_spec[i] = arg self.arg_spec.update(kwargs) self.omit_self = False
class ExamineArgumentsDecorator(object): ''' A base class for decorators that work by examining the arguments passed to functions and optionally altering them. It can be a basis for many useful decorators, like ones that automatically convert between certain types (e.g. database record identifiers and ORM objects), validate arguments (like the @expects decorator), and so on. ''' def __init__(self, *args, **kwargs): ''' Constructor. It accepts arbitrary arguments and uses them to build "argument specification" which will be used when calling decorated function. ''' self.arg_spec = ArgumentSpec() for i, arg in enumerate(args): self.arg_spec[i] = arg self.arg_spec.update(kwargs) self.omit_self = False def __call__(self, func): ''' Performs the actual decoration of given function. ''' if not inspect.isroutine(func): raise TypeError, "%s can only decorate functions" % type(self).__name__ self._improve_argument_spec(func) @functools.wraps(func) def wrapped(*args, **kwargs): args, kwargs = self._examine_arguments(args, kwargs) return func(*args, **kwargs) return wrapped def _improve_argument_spec(self, func): ''' Uses the actual arguments of function to improve the argument specification which was saved with the decorator object during its creation. The improved version should work regardless of how the argument was used (positionally or via keyword) in function call. ''' arg_names, varargs_name, kwargs_name, _ = inspect.getargspec(func) first_arg_is_self = len(arg_names) > 0 and arg_names[0] == 'self' is_unbound_method = not inspect.ismethod(func) and first_arg_is_self if is_unbound_method: self.omit_self = True if first_arg_is_self: arg_names = arg_names[1:] # 'self' is not part of argument spec if len(arg_names) > len(self.arg_spec): raise TypeError("Expected a function with %s argument(s), found only %s" % (len(self.arg_spec), len(arg_names))) for i, arg_name in enumerate(arg_names): arg_type = self.arg_spec.get(i) or self.arg_spec.get(arg_name) self.arg_spec[i] = arg_type self.arg_spec[arg_name] = arg_type if varargs_name: self.arg_spec.allows_varargs = True if kwargs_name: self.arg_spec.allows_kwargs = True def _examine_arguments(self, varargs, kwargs): ''' Goes through the positional and keywords arguments and invokes the (overridden) _process_argument method. @return: Processed arguments, as tuple of (args, kwargs) ''' if self.omit_self: omitted_self = varargs[0] varargs = varargs[1:] # omit 'self' new_varargs = range(0, len(varargs)) new_kwargs = {} arg_collections = [(enumerate(varargs), new_varargs), (kwargs.iteritems(), new_kwargs)] for arg_kvpairs, dest_args in arg_collections: for key, arg in arg_kvpairs: spec = self.arg_spec[key] processed = self._process_argument(spec, arg) dest_args[key] = processed if self.omit_self: new_varargs.insert(0, omitted_self) return (new_varargs, new_kwargs) def _process_argument(self, spec, actual): ''' Processes a single argument and returns it (possibly altered). @note: This method should be overriden in subclasses. @param spec: The specification object for this argument, passed to decorator's constructor @param actual: Actual value for argument, received in call to decorated function @return: Processed argument which will be passed to decorated functon ''' # override it in subclassess return actual