-
Notifications
You must be signed in to change notification settings - Fork 1
/
__init__.py
228 lines (189 loc) · 8.41 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
from collections import OrderedDict, namedtuple
from itertools import count
from types import MappingProxyType
'''
A simpler Enum for Python 3.
(c) 2013 Andrew Cooke, [email protected];
released into the public domain for any use, but with absolutely no warranty.
'''
def names():
'''Provide a value for each enum which is the name itself.'''
def value(name):
return name
return value
def from_counter(start, step=1):
'''Provide a value for each enum from a counter.'''
def outer():
counter = count(start, step)
def value(name):
nonlocal counter
return next(counter)
return value
return outer
from_one = from_counter(1)
from_one.__doc__ = 'Provide a value for each enum that counts from one.'
from_one.__name__ = 'from_one'
from_zero = from_counter(0)
from_zero.__doc__ = 'Provide a value for each enum that counts from zero.'
from_zero.__name__ = 'from_zero'
def bits():
'''Provide a value for each enum that is a distinct bit (1, 2, 4, etc).'''
count = 0
def value(name):
nonlocal count
count += 1
return 2 ** (count - 1)
return value
# Used to detect EnumMeta creation in the dict. If Enum is false then we
# disable implicit values.
Enum = None
def dunder(name):
'''Test for special names.'''
return name[:2] == name[-2:] == '__'
class ExplicitError(Exception): pass
ERR_MSG = 'Implicit scope support simple names only - no assignment or evaluation of expressions'
class Explode(int):
def __getattribute__(cls, item):
raise ExplicitError(ERR_MSG)
def __call__(self, *args, **kwargs):
raise ExplicitError(ERR_MSG)
class ClassDict(OrderedDict):
'''
This is the dictionary used while creating Enum instances. It provides
default values when `implicit` is true. This can either be enabled by
default, or within a `with` context.
'''
def __init__(self, implicit=False, values=names):
'''Setting `implicit` will provide default values from `values`.'''
super().__init__()
self.implicit = implicit
self.always_implicit = implicit
self.values = values()
def __enter__(self):
'''Enable implicit values within a `with` context.'''
self.implicit = True
def __exit__(self, exc_type, exc_val, exc_tb):
'''Disable implicit values on leaving a `with` context.'''
self.implicit = False
if exc_val: raise ExplicitError(ERR_MSG) from exc_val
def __getitem__(self, name):
'''Provide an item from the dictionary. Values are created if
`implicit` is currently true.'''
if name not in self:
if self.implicit and Enum and not dunder(name):
super().__setitem__(name, self.values(name))
if self.always_implicit: return Explode()
elif name == 'implicit':
return self
return super().__getitem__(name)
def __setitem__(self, name, value):
'''Set a value in the dictionary. Setting is disabled for user
values (not dunders) if `implicit` is true. This helps avoid
confusion from expressions involving shadowed global names.'''
if self.implicit and Enum and not dunder(name):
raise ExplicitError('Cannot use explicit value for %s' % name)
return super().__setitem__(name, value)
def split(self):
'''Separate the enums from the special values (dunders and
descriptors; we assume the latter are methods).'''
enums, others = OrderedDict(), dict()
for name in self:
value = self[name]
if dunder(name) or hasattr(value, '__get__'):
others[name] = value
else:
enums[name] = value
return enums, others
class EnumMeta(type):
'''
This does three main things: (1) it manages the construction of both
the Enum class and its sub-classes via `__prepare__` and `__new__`;
(2) it delegates the `dict` API to the `cls._enums` member so that
classes look like dictionaries; (3) it provides retrieval of named tuples
via `__call__`.
'''
def __init__(metacls, cls, bases, dict, **kargs):
'''Called during class construction. Drop kargs.'''
super().__init__(cls, bases, dict)
@classmethod
def __prepare__(metacls, name, bases, implicit=True, values=names, **kargs):
'''Provide the class dictionary (which provides implicit values).'''
return ClassDict(implicit=implicit, values=values)
def __new__(metacls, name, bases, prepared, allow_aliases=False, **kargs):
'''Create the class and then the named tuples, saving the latter in
the former.'''
enums, others = prepared.split()
cls = super().__new__(metacls, name, bases, others)
cls._enums_by_name, cls._enums_by_value = {}, OrderedDict()
for name in enums:
value = enums[name]
enum = cls.__new__(cls, name, value)
# handle aliases
if value in cls._enums_by_value:
if allow_aliases:
cls._enums_by_name[name] = cls._enums_by_value[value]
else:
raise ValueError('Duplicate value (%r) for %s and %s' %
(value, cls._enums_by_value[value].name, name))
else:
cls._enums_by_value[value] = enum
cls._enums_by_name[name] = enum
# build the delegate from values as that does not include aliases
cls._enums = MappingProxyType(
OrderedDict((enum.name, enum.value)
for enum in cls._enums_by_value.values()))
return cls
# Needed to avoid calling len() during creation?
def __bool__(cls): return True
# Delegate dictionary methods.
def __contains__(cls, name): return name in cls._enums_by_name # aliases
def __iter__(cls): return cls._enums.__iter__()
def __getitem__(cls, name): return cls._enums_by_name[name].value # aliases
def __len__(cls): return cls._enums.__len__()
def keys(cls): return cls._enums.keys()
def values(cls): return cls._enums.values()
def items(cls):
'''This can be seen in two ways. As a dictionary method it returns
`(name, value)` pairs. But it also returns a list of named tuples
that are the enumerations themselves.'''
return iter(cls._enums_by_value[value] for value in cls._enums_by_value)
def __getattr__(cls, name):
'''Provide access to named tuples.'''
try: return cls._enums_by_name[name]
except KeyError: raise AttributeError(name)
def __call__(cls, name=None, value=None):
'''Retrieve named tuples by name or value. We also special case
calling with an existing instance.'''
if type(name) is cls:
if value is None or value == name.value: return name
elif value is None:
if name is None: raise ValueError('Give name or value')
if name in cls._enums_by_name: return cls._enums_by_name[name]
raise ValueError('No name %r' % name)
elif name is None:
if value in cls._enums_by_value: return cls._enums_by_value[value]
raise ValueError('No value %r' % value)
elif name in cls._enums_by_name:
enum = cls._enums_by_name[name]
if value in cls._enums_by_value and \
enum is cls._enums_by_value[value]: return enum
raise ValueError('Inconsistent name (%r) and value (%r)' %
(name, value))
def __str__(cls):
return ', '.join(cls.keys())
def __repr__(cls):
return '%s(%s)' % (cls.__name__,
', '.join('%s: %r' % (enum.name, enum.value)
for enum in cls.items()))
class Enum(namedtuple('Enum', 'name, value'), metaclass=EnumMeta):
'''
The super class for enumerations. The body of sub-classes should
typically contain a list of enumeration names.
'''
def __new__(cls, *args, **kwargs):
'''Called on instance creation and by pickle. We try __call__ first
so that unpickling retrieves an existing instance. If that fails
then we create a new instance.'''
try: return cls.__call__(*args, **kwargs)
except ValueError: return super().__new__(cls, *args, **kwargs)
def _make(self): raise TypeError('Enum contents cannot be extended')