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