""" duper - A super-duper super replacement, and related utilities. 'duper' and 'classduper' are attribute-access-cooperative versions of the buil-in 'super' type. 'superdesc' is a base class for writting descriptors for cooperative attributes. 'coopproperty', an example of a subclass of 'superdesc', is a simple cooperative replacement for the built-in 'property'. 'objectWithFillin' is a baseclass for classes that want the defining class and name of an attribute to be filled in for attributes defined in the class. Copyright Nathan Srebro, MIT, July 2002. November 2002 (superdesc and related classes added). nati@mit.edu http://www.ai.mit.edu/~nati/Python """ __all__ = ['SubclassError','duper','classduper','superdesc','coopproperty','objectWithFillin','FillinType'] from inspect import getmro class SubclassError(TypeError): pass def _PyType_suffix_lookup(origclass, name, thisclass = None): "Lookup name in MRO of origclass, starting with thisclass. Returns descriptor." # This is an extension of typeobject.c:_PyType_Lookup mro = iter(getmro(origclass)) if thisclass is not None: for base in mro: if base is thisclass: break else: raise SubclassError,"%r is not a subclass of %r"%(thisclass,origclass) notfound = object() for base in mro: descr = base.__dict__.get(name,notfound) if descr is not notfound: return descr raise AttributeError,"%r has no attribute %s beyond %r"%(origclass,name,thisclass) def PyDescr_IsData(descr): "True is 'descr' is a data descriptor, i.e. supports __set__" return hasattr(descr,'__set__') class duper(object): # We don't really use any of super's methods """A super-duper super for cooperative attribute access through descriptors duper is a more accurate replacement for super. Unlike super, duper supports also setting and deleting attributes, and will revert to the instance dictionary when the supered-class would. duper(tp,obj).x duper(tp,obj).x = value del duper(tp,obj).x gets/sets/deletes attribute 'x' as if 'obj' was an instance of the cooperative baseclass of 'tp': the baseclass if there is only one, and the suffix of the mro of obj.__class__ in general. 'obj' must be an instance of 'tp' ('tp' can be either a type or a class). More accurately, the attribute is accessed as if 'obj' was an instance of a type (not a class!) with its bases being the suffix of the mro of obj.__class__, starting just after 'tp', but with attribute-access reset to behave like 'object'. This 'cooperative baseclass' can be explicitly specified as follows: mro = getmro(obj.__class__) assert mro[i] = tp class cooperativebase(object+mro[i+1:]): __metatype__ = type __getattribute__ = object.__getattribute__ __setattr__ = object.__setattr__ def __getattr__(self,sttr): raise AttributeError Specific points to Note: - Ignores __getattr__,__getattribute__,__setattr__ and __delattr__ customizations to types/classes (assumes all types use PyObject_Generic(Get|Set)Attr and classes don't mess with these) - Assumes all superclasses of obj support instance dictionaries just like obj's class. In particular, if a descriptor is not found, or if the descriptor is a data descriptor, then the instance dictionary will be looked up / accessed (just like GenericGetAttr uses it). This behavior can be modified by two optional flags: duper(type,obj,dictunlessdata=1,fallbackondict=1) If dictunlessdata is False, if any descriptor is found, even a data descriptor, it will always be used, instead of the instance dictionary. If fallbackondict is False, attribute access will fail if no descriptor is found (instead of reverting to the instance dictionary). Unbound dupers: duper(type) creates an unbound duper, which can be used as a descriptor. As an instance attribute it is a duper bound to the instance, and as a class attribute it is a classduper. Unlike super, duper does not support duper(type,subtype). Use 'classduper' instead. Limitations: - In Python 2.2.1, delete does not work well with built-in descriptors since they do not expose __delete__. This works fine with a patched-up Python that exposes __delete__, as well as with Python 2.2.2 (which includes the patch). - BUG: accessing __reduce__, and perhaps other special methods, through duper currently does not work--- use super instead. """ __slots__ = ['__thisclass__','__self__','__dictunlessdata__','__fallbackondict__'] def __init__(self,thisclass,obj=None,dictunlessdata=1,fallbackondict=1): duper.__thisclass__.__set__(self,thisclass) duper.__self__.__set__(self,obj) # We don't check validity of obj here.... we'll complain later duper.__dictunlessdata__.__set__(self,dictunlessdata) duper.__fallbackondict__.__set__(self,fallbackondict) def __repr__(self): if self.__self__ is None: return ""%self.__thisclass__ else: return ""%(self.__thisclass__,self.__self__) def __get__(self,obj,tp=None): "Binds an unbound duper" if self.__self__ is not None: return self if obj is None: return classduper(self.__thisclass__,tp, self.__dictunlessdata__,self.__fallbackondict__) return self.__class__(self.__thisclass__,obj, self.__dictunlessdata__,self.__fallbackondict__) def __getdescr(self,attr): "Looks up descriptor of attr" obj = self.__self__ if obj is None: raise ValueError,"Unbound duper" return _PyType_suffix_lookup(obj.__class__,attr,self.__thisclass__) def __getfromdict(self,attr): # We should actually check if anything beyond thisclass supports a dict try: return self.__self__.__dict__[attr] except (AttributeError, KeyError): raise AttributeError, "%r has no attribute '%s'"%(self,attr) def __getattr__(self,attr): try: descr = self.__getdescr(attr) except AttributeError: if self.__fallbackondict__: return self.__getfromdict(attr) else: raise AttributeError, "%r has no attribute '%s'"%(self,attr) try: getter = descr.__get__ except AttributeError: try: return self.__getfromdict(attr) except AttributeError: return descr if self.__dictunlessdata__ and not PyDescr_IsData(descr): try: return self.__getfromdict(attr) except AttributeError: pass return getter(self.__self__,self.__self__.__class__) class __deleteflag: pass def __setattr__(self,attr,val=__deleteflag): try: descr = self.__getdescr(attr) if val is self.__deleteflag: deller = descr.__delete__ else: setter = descr.__set__ except AttributeError: if self.__fallbackondict__: dict = self.__self__.__dict__ if val is self.__deleteflag: try: del dict[attr] except KeyError: raise AttributeError,attr else: dict[attr]=val # need to deal better with exceptions in above, e.g. convert to AttributeError else: raise AttributeError,"%r has no attribute '%s'"%(self,attr) else: # proper slot found in descriptor if val is self.__deleteflag: deller(self.__self__) else: setter(self.__self__,val) __delattr__ = __setattr__ class classduper(object): """A cooperative class super for class-attributes duper(type,subtype).x returns the attribute 'x' of the cooperative baseclass of 'type' with respect to 'subtype' (i.e. the baseclass if there is only one, and the suffix of the mro of 'subtype' otherwise), but associates it with the type 'subtype'.""" # Note that super's support for super(type,subtype) is # problematic: # 1) it is possible to have isinstance(a,b) and issubclass(a,b), # e.g. if b is a subclass of 'type', in which case it is ambiguous # which behavior is desired # 2) super(a,b).x calls c.x.__get__(b,b), instead of c.x.__get__(None,b). __slots__ = ['__thisclass__','__subclass__','__dictunlessdata__','__fallbackondict__'] def __init__(self,thisclass,subclass,dictunlessdata=1,fallbackondict=1): self.__thisclass__ = thisclass self.__subclass__ = subclass # We don't check validity of subclass here.... we'll complain later classduper.__dictunlessdata__.__set__(self,dictunlessdata) classduper.__fallbackondict__.__set__(self,fallbackondict) # These two donn't affect classduper itself, but only dupers bound from it def __repr__(self): return ""%(self.__thisclass__,self.__subclass__) def __get__(self,obj,tp=None): if obj is None: return self if isinstance(obj,self.__subclass__): return duper(self.__thisclass__,obj,self.__dictunlessdata__,self.__fallbackondict__) raise TypeError def __getattr__(self,attr): descr = _PyType_suffix_lookup( self.__subclass__,attr,self.__thisclass__) try: getter = descr.__get__ except AttributeError: return descr else: return getter(None,self.__subclass__) class superdesc(object): """A base class for descriptors in a subclasses that want to coexist with descriptors that lives in base classes of the subclass. That is: Is an attribute x is described by a superdesc descriptor in class C, then any attribute access to x will be deferred to however the attribute access is dealt with in the baseclass(es) of C, following the standard mro. That is, attribute access for x will behave as for an otherwise empty class based on all the base classes of C. This includes reading from, and setting, the instance dictionary where appropriate (note, though, that . Thus, a descriptor that subclasses superdesc can cooperate with attribute access for the same attribute in baseclasses of the class in which it is defined. This can be done by calling the __get__, __set__ and __delete__ methods of superdesc, e.g. using super. E.g.: class mycooperativedescriptor(superdesc): def __get__(self,obj,tp=None): # do my stuff return super(mycooperativedescriptor,self).__get__(obj,tp) Creation/initiation: superdesc(thisclass,name) is a superdesc descriptor for attribute named 'name' in class 'thisclass'. (Additional arguments, including keyword arguments, are cooperatively passed to other base classes) Methods: __get__, __set__, __delete__ Data attributes: __thisclass__ is the class in which this descriptor lives, __name__ is the name of the attribute Note: When setting a descriptor in a class definition, the class being defined needs to be passed to the constructor (as 'thisclass'). This is generally not possible, since the class does not yet exist. A possible workaround is to manually add the descriptor to the class dictionary outside the class definition, e.g.: class C(object): pass C.x = superdesc(C,'x') Another option is to derive the containing class from objectWithFillin, and not specify 'thisclass' and 'name'. They will be filled in durring class creation: class C(objectWithFillin): x = superdesc() Note that not specifying 'thisclass' and 'name' without deriving the class from objectWithFillin will result in strange errors when the attribute is used. """ __slots__ = ['__thisclass__','__name__'] def __init__(self,name=None,thisclass=None,*args,**kwargs): self.__thisclass__ = thisclass self.__name__ = name super(superdesc, self).__init__(*args,**kwargs) # I should probably add error-handling code from # thisclass,name=None in the following routines. def __get__(self,obj,tp=None): if obj is not None: return getattr(duper(self.__thisclass__,obj),self.__name__) else: return getattr(classduper(self.__thisclass__,tp),self.__name__) def __set__(self,obj,val): setattr(duper(self.__thisclass__,obj),self.__name__,val) def __delete__(self,obj): delattr(duper(self.__thisclass__,obj),self.__name__) class coopproperty(superdesc): """A coopperative versoin of the built-in 'property' descriptor type: unspecified access methods default to using the access methods for baseclasses of the class in which the attribute is defined. """ # need to expand usage docs __slots__ = ['fget','fset','fdel','__doc__'] def __init__(self,fget=getattr,fset=setattr,fdel=delattr, doc=None,*args,**kwargs): self.fget = fget ; self.fset = fset ; self.fdel = fdel # self.__doc__ = doc # this evokes some strange bug super(coopproperty,self).__init__(*args,**kwargs) def __get__(self,obj,tp=None): if obj is not None: if self.fget is None: raise AttributeError,"unreadable attribute" if self.fget is not getattr: return self.fget(obj) return super(coopproperty,self).__get__(obj,tp) def __set__(self,obj,val): if self.fset is None: raise AttributeError,"cann't set attribute" elif self.fset is setattr: super(coopproperty,self).__set__(obj,val) else: self.fset(obj,val) def __del__(self,obj): if self.fdel is None: raise AttributeError,"cann't del attribute" elif self.fdel is delattr: super(coopproperty,self).__del__(obj) else: self.fdel(obj) class FillinType(type): def __init__(self,name,bases,dct): super(FillinType,self).__init__(name,bases,dct) for attrname,attr in self.__dict__.items(): try: if attr.__thisclass__ is None: attr.__thisclass__ = self except AttributeError: pass try: if attr.__name__ is None: attr.__name__ = attrname except AttributeError: pass class objectWithFillin(object): __metaclass__ = FillinType