""" A base-class (and meta-class) for classes with cacheable, invalidatebale, calculated attributes, and a bit more. Copyright Nathan Srebro, MIT, 2002 nati@mit.edu http://www.ai.mit.edu/~nati/Python """ __all__ = [ 'DynamicObject', 'DynamicObjectX', 'CalculatedProperty', 'CumulativeAttribute', 'NewCumulativeAttribute', 'ExternalInvalidation'] from types import FunctionType,MethodType,ClassType,TypeType import sys,inspect,weakref,copy from inspect import getargspec as _orig_getargspec def getargspec(func): # corrected version of getargspec args, varargs, kwargs, defaults = _orig_getargspec(func) if not defaults: defaults = () return args, varargs, kwargs, defaults from superduper import * def matchdefaults(argnames,defaults=None): """Returns a dictionary of argname:default-value with all argument with a default value. Can be called with a function object as a single argument, or with two arguments: matchdefaults(getargspec(f)[0],getargspec(f)[3]).""" if defaults is None: argnames,dummy,dummy,defaults = getargspec(argnames) d = {} for name,val in zip(argnames[-len(defaults):],defaults): d[name]=val return d class CumulativeAttribute(list): def __init__(self,*args): super(CumulativeAttribute,self).__init__(args) class NewCumulativeAttribute(CumulativeAttribute): pass class ExternalInvalidation(object): slots = ['__name__','dependencies'] def __init__(self,*args): self.dependencies = args self.__name__ = None # will be filled in def __invalidate__(self,obj): for callback in getattr(obj,self.__name__).values(): callback() return 0 # Todo: change this to a weak list instead of a weak value dict def __get__(self,obj,tp=None): if obj is None: return self return obj.__dict__.setdefault(self.__name__,weakref.WeakValueDictionary()) def __set__(self,obj,val): raise TypeError def __del__(self,obj): raise TypeError class MetaDynamic(FillinType): autoinvprefix = '_auto_invalidate_list_' def __init__(self,name,bases,dct): super(MetaDynamic,self).__init__(name,bases,dct) # Unite the invalidation dictionaries of the bases c_addtoattrs_dict = self._addtoattrs_dict = {} for base in bases: for attr,attr_dict in getattr(base,'_addtoattrs_dict',{}).items(): c_addtoattrs_dict.setdefault(attr,{}).update(attr_dict) # deal with specially specified properties for attrname,attr in self.__dict__.items(): # we might change dct size if not hasattr(self,attrname): continue # we already removed it if isinstance(attr,FunctionType): argnames, args, kwargs, defaults = getargspec(attr) if args == 'auto_attr': additional_args = matchdefaults(argnames,defaults) try: attr = CalculatedProperty(self,attrname,attr, arglist = argnames[1:len(argnames)-len(defaults)], **additional_args) except: exc_type, exc_value = sys.exc_info()[:2] s = '%s while defining %s.%s'%(exc_value, name, attrname) try: s += ' in %s(%d)'%(inspect.getsourcefile(attr), inspect.getsourcelines(attr)[1]) except: pass raise exc_type,s setattr(self,attrname,attr) elif args == 'class_auto_attr': attr = attr(self) setattr(self,attrname,attr) elif args == 'sideeffect': attr = SideeffectProperty(attr,thisclass=self,name=attrname,*defaults) setattr(self,attrname,attr) elif isinstance(attr,NewCumulativeAttribute): c_addtoattrs_dict[attrname] = {} elif isinstance(attr,CumulativeAttribute): c_addtoattrs_dict.setdefault(attrname,{}) elif isinstance(attr,ExternalInvalidation): # Never copy external dependencies (since they depend # on the original, not the copy c_addtoattrs_dict['__nevercopylist__'][attrname]=1 # add this attr to the invalidation list of its dependencies for dep in getattr(attr,'dependencies',()): c_addtoattrs_dict.setdefault(self.autoinvprefix+dep,{})[attrname]=1 for attrname,attr_dict in c_addtoattrs_dict.items(): # TODO: change the following to look only in self'd dict for val in getattr(self,attrname,[]): attr_dict[val]=1 setattr(self,attrname,attr_dict.keys()) class DynamicObject(object): __metaclass__ = MetaDynamic def __delattr__(self,attr): self.invalidate_attr(attr) super(DynamicObject,self).__delattr__(attr) def __setattr__(self,attr,value): # Note that setattr overrides any descriptors. The super'ed # call here will call the appropriate descriptor. (same for delattr) super(DynamicObject,self).__setattr__(attr,value) self.invalidate_attr(attr) def invalidate_attr(self,attr): invalidies = getattr(self,MetaDynamic.autoinvprefix+attr,[]) for argname in invalidies: if self.invalidate_attr(argname): try: del self.__dict__[argname] except KeyError: pass # OK to fail deleting at this point return 1 # delete the invalidated attr def suggest_cach(self,**kwargs): for attr,val in kwargs.items(): self.__class__.__dict__['attr'].suggest_cache(self,val) def force_cache(self,**kwargs): for attr,val in kwargs.items(): self.__class__.__dict__['attr'].force_cache(self,val) def __getstate__(self): state = copy.copy(self.__dict__) for name in self.__nevercopylist__: try: del state[name] except KeyError: pass for name in self.__copyfollowslist__: try: attr = state[name] except KeyError: pass else: state[name] = copy.copy(attr) return state __nevercopylist__ = NewCumulativeAttribute() __copyfollowslist__ = NewCumulativeAttribute() class DynamicObjectX(DynamicObject): "Allows customizing attribute invalidation" def invalidate_attr(self,attr): try: desc = getattr(self.__class__,attr) invalidator = desc.__invalidate__ except AttributeError: invalidator = None if invalidator: return invalidator(self) else: return DynamicObject.invalidate_attr(self,attr) class CalculatedProperty(coopproperty): slots = ['fcalc','arglist','dependencies', 'invalidate_attrname','cacheflag_name','cacheflag_default'] def __init__(self, thisclass, name, fcalc, cache=0, doc=None, arglist=None, depends_on=[], independent_of=[], invalidate_attrname=None, cacheflag_name=None, fset=setattr, fdel=delattr): self.fcalc = fcalc if doc is None: doc = fcalc.__doc__ super(CalculatedProperty,self).__init__( thisclass=thisclass, name=name, fset=fset, fdel=fdel, doc=doc) if arglist is None: arglist = self.extract_arglist(fcalc) self.arglist = arglist self.dependencies = [attr for attr in arglist if attr not in independent_of] + depends_on self.cacheflag_default = cache if cacheflag_name is None: cacheflag_name = '_auto_cache_' + name if invalidate_attrname is None: invalidate_attrname = MetaDynamic.autoinvprefix + name self.cacheflag_name = cacheflag_name self.invalidate_attrname = invalidate_attrname def extract_arglist(self,fcalc): # arglist = what other arguments does fcalc expect if isinstance(fcalc,FunctionType): # For a func: additional args beyond first return getargspec(fcalc)[0][1:] elif isinstance(fcalc,MethodType): fullarglist = getargspec(fcalc.im_func)[0] if fcalc.im_self: return fullarglist[2:] # 1st arg (self) already bound else: return fullarglist[1:] # unbound method-- just like a func elif isinstance(fcalc,(TypeType,ClassType)): # fcalc is a type, i.e. initialize new object try: return getargspec(fcalc.__init__.im_func)[0][2:] except: return [] # add also some treatment for instances with __call__ else: arglist = [] def suggest_cache(self,obj,value): if getattr(obj,self.cacheflag_name,self.cacheflag_default): obj.__dict__[self.__name__] = value def force_cache(self,obj,value): obj.__dict__[self.__name__] = value def calculate(self,obj): return self.fcalc(obj,*[getattr(obj,argname) for argname in self.arglist]) def __get__(self,obj,type=None): try: return obj.__dict__[self.__name__] except KeyError: value = self.calculate(obj) self.suggest_cache(obj,value) return value except AttributeError: if obj is None: return self # class attribute else: raise class SideeffectProperty(superdesc): __slots__ = ['default','sideeffect','__doc__'] def __init__(self,sideeffect,default=None,doc=None,*args,**kwargs): self.default = default ; self.sideeffect = sideeffect if doc is None: doc = sideeffect.__doc__ self.__doc__ = doc super(SideeffectProperty,self).__init__(*args,**kwargs) def __get__(self,obj,tp=None): try: super(SideeffectProperty,self).__get__(obj,tp) except AttributeError: return self.default def __set__(self,obj,val): super(SideeffectProperty,self).__set__(obj,val) self.sideeffect(obj,val) # no __del__