1 2_NOT_SET = object() 3 4 5class Slot: 6 """A descriptor that provides a slot. 7 8 This is useful for types that can't have slots via __slots__, 9 e.g. tuple subclasses. 10 """ 11 12 __slots__ = ('initial', 'default', 'readonly', 'instances', 'name') 13 14 def __init__(self, initial=_NOT_SET, *, 15 default=_NOT_SET, 16 readonly=False, 17 ): 18 self.initial = initial 19 self.default = default 20 self.readonly = readonly 21 22 # The instance cache is not inherently tied to the normal 23 # lifetime of the instances. So must do something in order to 24 # avoid keeping the instances alive by holding a reference here. 25 # Ideally we would use weakref.WeakValueDictionary to do this. 26 # However, most builtin types do not support weakrefs. So 27 # instead we monkey-patch __del__ on the attached class to clear 28 # the instance. 29 self.instances = {} 30 self.name = None 31 32 def __set_name__(self, cls, name): 33 if self.name is not None: 34 raise TypeError('already used') 35 self.name = name 36 try: 37 slotnames = cls.__slot_names__ 38 except AttributeError: 39 slotnames = cls.__slot_names__ = [] 40 slotnames.append(name) 41 self._ensure___del__(cls, slotnames) 42 43 def __get__(self, obj, cls): 44 if obj is None: # called on the class 45 return self 46 try: 47 value = self.instances[id(obj)] 48 except KeyError: 49 if self.initial is _NOT_SET: 50 value = self.default 51 else: 52 value = self.initial 53 self.instances[id(obj)] = value 54 if value is _NOT_SET: 55 raise AttributeError(self.name) 56 # XXX Optionally make a copy? 57 return value 58 59 def __set__(self, obj, value): 60 if self.readonly: 61 raise AttributeError(f'{self.name} is readonly') 62 # XXX Optionally coerce? 63 self.instances[id(obj)] = value 64 65 def __delete__(self, obj): 66 if self.readonly: 67 raise AttributeError(f'{self.name} is readonly') 68 self.instances[id(obj)] = self.default # XXX refleak? 69 70 def _ensure___del__(self, cls, slotnames): # See the comment in __init__(). 71 try: 72 old___del__ = cls.__del__ 73 except AttributeError: 74 old___del__ = (lambda s: None) 75 else: 76 if getattr(old___del__, '_slotted', False): 77 return 78 79 def __del__(_self): 80 for name in slotnames: 81 delattr(_self, name) 82 old___del__(_self) 83 __del__._slotted = True 84 cls.__del__ = __del__ 85 86 def set(self, obj, value): 87 """Update the cached value for an object. 88 89 This works even if the descriptor is read-only. This is 90 particularly useful when initializing the object (e.g. in 91 its __new__ or __init__). 92 """ 93 self.instances[id(obj)] = value 94 95 96class classonly: 97 """A non-data descriptor that makes a value only visible on the class. 98 99 This is like the "classmethod" builtin, but does not show up on 100 instances of the class. It may be used as a decorator. 101 """ 102 103 def __init__(self, value): 104 self.value = value 105 self.getter = classmethod(value).__get__ 106 self.name = None 107 108 def __set_name__(self, cls, name): 109 if self.name is not None: 110 raise TypeError('already used') 111 self.name = name 112 113 def __get__(self, obj, cls): 114 if obj is not None: 115 raise AttributeError(self.name) 116 # called on the class 117 return self.getter(None, cls) 118