import inspect import itertools as it from collections import deque __all__ = ['defaultsfrom'] #======= demos =============================================================== def demo_func(): '''Reuse function default values.''' def genericsort(iterable, cmp=None, key=None, reverse=False): raise NotImplementedError() @defaultsfrom(genericsort) def timsort(iterable, cmp, key, reverse): return sorted(iterable, cmp, key, reverse) s = ['hello', 'WORLD'] timsort(s) timsort(s, key=str.lower) def demo_class(): '''Reuse method default values.''' import logging class GenericLogger(object): def __init__(self, name, level=logging.WARNING, handlers=(), filters=()): self._logger = logging.getLogger(name) self._logger.setLevel(level) for h in handlers: self._logger.addHandler(h) for f in filters: self._logger.addFilter(f) class FancyLogger(GenericLogger): @defaultsfrom(GenericLogger) def __init__(self, name, level, handlers, filters, bells=None, whistles=None, debug=False): super(FancyLogger,self).__init__(name, level, handlers, filters) for attr in 'bells', 'whistles', 'debug': setattr(self, '_'+attr, eval(attr)) mylog = FancyLogger('demo', filters=[logging.Filter()], debug=True) #======= defaultsfrom ======================================================== def defaultsfrom(funcOrClass): '''Return a decorator d so that d(func) updates func's default arguments. If funcOrClass is a function (or method) 'foo', its default arguments are 'inherited' by any function 'bar' decorated by the returned decorator: >>> def foo(a, x=0, y=''): pass >>> @defaultsfrom(foo) ... def bar(a, b, y, x, z=None): return a,b,y,x,z >>> bar(1,2,3,4,5) (1, 2, 3, 4, 5) >>> bar(1,2,3,4) (1, 2, 3, 4, None) >>> bar(1,2,3) (1, 2, 3, 0, None) >>> bar(1,2) (1, 2, '', 0, None) Any default arguments redefined by 'bar' are not inherited by 'foo': >>> @defaultsfrom(foo) ... def zong(a, x, y=-1): # y redefined ... return a,x,y >>> zong(2) (2, 0, -1) Default arguments (inherited or not) cannot precede non-default ones: >>> @defaultsfrom(foo) ... def zap(a, x, b, y): # b is not a default arg; x cannot be inherited ... return a,x,b,y Traceback (most recent call last): ... TypeError: ... If funcOrClass is a class, its method with the same name with the decorated function is handled as above: >>> class Base(object): ... def __init__(self, a, b=0, c=None): pass >>> class Derived(Base): ... @defaultsfrom(Base) # equivalent to Base.__init__ ... def __init__(self, a, b, c): print (a,b,c) >>> d = Derived(1) (1, 0, None) ''' def decorator(newfunc): if inspect.isclass(funcOrClass): func = getattr(funcOrClass, newfunc.__name__) else: func = funcOrClass args,_,_,defaults = inspect.getargspec(func) # map each default argument of func to its value arg2default = dict(zip(args[-len(defaults):],defaults)) newargs,_,_,newdefaults = inspect.getargspec(newfunc) if newdefaults is None: newdefaults = () nondefaults = newargs[:len(newargs)-len(newdefaults)] # starting from the last non-default argument towards the first, as # long as the non-defaults of newfunc are default in func, make them # default in newfunc too iter_nondefaults = reversed(nondefaults) newdefaults = deque(newdefaults) for arg in it.takewhile(arg2default.__contains__, iter_nondefaults): newdefaults.appendleft(arg2default[arg]) # all inherited defaults should be placed together; no gaps allowed for arg in it.ifilter(arg2default.__contains__, iter_nondefaults): raise TypeError('%s cannot inherit the default arguments of ' '%s' % (newfunc, func)) newfunc.func_defaults = tuple(newdefaults) return newfunc return decorator if __name__ == '__main__': import doctest doctest.testmod(optionflags=doctest.ELLIPSIS) demo_func() demo_class()