| |
| _NOT_SET = object() |
| |
| |
| class Slot: |
| """A descriptor that provides a slot. |
| |
| This is useful for types that can't have slots via __slots__, |
| e.g. tuple subclasses. |
| """ |
| |
| __slots__ = ('initial', 'default', 'readonly', 'instances', 'name') |
| |
| def __init__(self, initial=_NOT_SET, *, |
| default=_NOT_SET, |
| readonly=False, |
| ): |
| self.initial = initial |
| self.default = default |
| self.readonly = readonly |
| |
| # The instance cache is not inherently tied to the normal |
| # lifetime of the instances. So must do something in order to |
| # avoid keeping the instances alive by holding a reference here. |
| # Ideally we would use weakref.WeakValueDictionary to do this. |
| # However, most builtin types do not support weakrefs. So |
| # instead we monkey-patch __del__ on the attached class to clear |
| # the instance. |
| self.instances = {} |
| self.name = None |
| |
| def __set_name__(self, cls, name): |
| if self.name is not None: |
| raise TypeError('already used') |
| self.name = name |
| try: |
| slotnames = cls.__slot_names__ |
| except AttributeError: |
| slotnames = cls.__slot_names__ = [] |
| slotnames.append(name) |
| self._ensure___del__(cls, slotnames) |
| |
| def __get__(self, obj, cls): |
| if obj is None: # called on the class |
| return self |
| try: |
| value = self.instances[id(obj)] |
| except KeyError: |
| if self.initial is _NOT_SET: |
| value = self.default |
| else: |
| value = self.initial |
| self.instances[id(obj)] = value |
| if value is _NOT_SET: |
| raise AttributeError(self.name) |
| # XXX Optionally make a copy? |
| return value |
| |
| def __set__(self, obj, value): |
| if self.readonly: |
| raise AttributeError(f'{self.name} is readonly') |
| # XXX Optionally coerce? |
| self.instances[id(obj)] = value |
| |
| def __delete__(self, obj): |
| if self.readonly: |
| raise AttributeError(f'{self.name} is readonly') |
| self.instances[id(obj)] = self.default # XXX refleak? |
| |
| def _ensure___del__(self, cls, slotnames): # See the comment in __init__(). |
| try: |
| old___del__ = cls.__del__ |
| except AttributeError: |
| old___del__ = (lambda s: None) |
| else: |
| if getattr(old___del__, '_slotted', False): |
| return |
| |
| def __del__(_self): |
| for name in slotnames: |
| delattr(_self, name) |
| old___del__(_self) |
| __del__._slotted = True |
| cls.__del__ = __del__ |
| |
| def set(self, obj, value): |
| """Update the cached value for an object. |
| |
| This works even if the descriptor is read-only. This is |
| particularly useful when initializing the object (e.g. in |
| its __new__ or __init__). |
| """ |
| self.instances[id(obj)] = value |
| |
| |
| class classonly: |
| """A non-data descriptor that makes a value only visible on the class. |
| |
| This is like the "classmethod" builtin, but does not show up on |
| instances of the class. It may be used as a decorator. |
| """ |
| |
| def __init__(self, value): |
| self.value = value |
| self.getter = classmethod(value).__get__ |
| self.name = None |
| |
| def __set_name__(self, cls, name): |
| if self.name is not None: |
| raise TypeError('already used') |
| self.name = name |
| |
| def __get__(self, obj, cls): |
| if obj is not None: |
| raise AttributeError(self.name) |
| # called on the class |
| return self.getter(None, cls) |