1
2 _NOT_SET = object()
3
4
5 class ESC[4;38;5;81mSlot:
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
96 class ESC[4;38;5;81mclassonly:
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)