-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathenum.py
310 lines (260 loc) · 10.8 KB
/
enum.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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# Copyright (C) 2004-2013 by Barry A. Warsaw
#
# This file is part of flufl.enum
#
# flufl.enum is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# flufl.enum is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
# for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with flufl.enum. If not, see <http://www.gnu.org/licenses/>.
#
# Author: Barry Warsaw <[email protected]>
"""Python enumerations."""
from __future__ import absolute_import, print_function, unicode_literals
import re
import sys
import warnings
from operator import itemgetter
COMMASPACE = ', '
SPACE = ' '
IDENTIFIER_RE = r'^[a-zA-Z_][a-zA-Z0-9_]*$'
class EnumMetaclass(type):
"""Meta class for Enums."""
def __init__(cls, name, bases, attributes):
"""Create an Enum class.
:param cls: The class being defined.
:param name: The name of the class.
:param bases: The class's base classes.
:param attributes: The class attributes.
"""
super(EnumMetaclass, cls).__init__(name, bases, attributes)
# Store EnumValues here for easy access.
cls._enums = {}
# Figure out if this class has a custom factory for building enum
# values. The default is EnumValue, but the class (or one of its
# bases) can declare a custom one with a special attribute.
factory = attributes.get('__value_factory__')
# Figure out the set of enum values on the base classes, to ensure
# that we don't get any duplicate values. At the same time, check the
# base classes for the special attribute.
for basecls in cls.__mro__:
if hasattr(basecls, '_enums'):
cls._enums.update(basecls._enums)
if hasattr(basecls, '__value_factory__'):
basecls_factory = basecls.__value_factory__
if factory is not None and basecls_factory != factory:
raise TypeError(
'Conflicting enum factory in base class: {}'.format(
basecls_factory))
factory = basecls_factory
# Set the factory default if necessary.
if factory is None:
factory = EnumValue
# For each class attribute, create an enum value and store that back
# on the class instead of the original value. Skip Python reserved
# names. Also add a mapping from the original value to the enum value
# instance so we can return the same object on conversion.
for attr in attributes:
if not (attr.startswith('__') and attr.endswith('__')):
value = attributes[attr]
enumval = factory(cls, value, attr)
if value in cls._enums:
other = cls._enums[value]
# Without this, sort order is undefined and causes
# unpredictable results for the test suite.
first = (attr if attr < other else other)
second = (other if attr < other else attr)
raise ValueError("Conflicting enum value '{}' "
"for names: '{}' and '{}'".format(
value, first, second))
# Store as an attribute on the class, and save the attr name.
setattr(cls, attr, enumval)
cls._enums[value] = attr
def __dir__(cls):
# For Python 3.2, we must explicitly convert the dict view to a list.
# Order is not guaranteed, so don't sort it.
return list(cls._enums.values())
def __repr__(cls):
# We want predictable reprs. Because base Enum items can have any
# value, the only reliable way to sort the keys for the repr is based
# on the attribute name, which must be Python identifiers.
return '<{0} {{{1}}}>'.format(cls.__name__, COMMASPACE.join(
'{0}: {1}'.format(value, key)
for key, value in sorted(cls._enums.items(), key=itemgetter(1))))
def __iter__(cls):
for value in sorted(cls._enums.values()):
yield getattr(cls, value)
def __getitem__(cls, item):
attr = cls._enums.get(item)
if attr is None:
# If this is an EnumValue, try it's .value attribute.
if hasattr(item, 'value'):
attr = cls._enums.get(item.value)
if attr is None:
# It wasn't value-ish -- try the attribute name. This was
# deprecated in LP: #1167091.
try:
warnings.warn('Enum[item_name] is deprecated; '
'use getattr(Enum, item_name)',
DeprecationWarning, 2)
return getattr(cls, item)
except (AttributeError, TypeError):
raise ValueError(item)
return getattr(cls, attr)
def __call__(cls, *args):
# One-argument calling is a deprecated synonym for getitem.
if len(args) == 1:
warnings.warn('MyEnum(arg) is deprecated; use MyEnum[arg]',
DeprecationWarning, 2)
return cls.__getitem__(args[0])
name, source = args
return _make(cls, name, source)
class EnumValue:
"""Class to represent an enumeration value.
EnumValue('Color', 'red', 12) prints as 'Color.red' and can be converted
to the integer 12.
"""
def __init__(self, enum, value, name):
self._enum = enum
self._value = value
self._name = name
def __repr__(self):
return '<EnumValue: {0}.{1} [value={2}]>'.format(
self._enum.__name__, self._name, self._value)
def __str__(self):
return '{0}.{1}'.format(self._enum.__name__, self._name)
def __int__(self):
return self._value
def __reduce__(self):
return getattr, (self._enum, self._name)
@property
def enum(self):
"""Return the class associated with the enum value."""
return self._enum
@property
def name(self):
"""Return the name of the enum value."""
return self._name
@property
def value(self):
"""Return the underlying value."""
return self._value
# Support only comparison by identity and equality. Ordered comparisions
# are not supported.
def __eq__(self, other):
return self is other
def __ne__(self, other):
return self is not other
def __lt__(self, other):
# In Python 3, returning NotImplemented from an ordered comparison
# operator will cause a TypeError to be raised. This doesn't work in
# Python 2 though, and you'd end up with working, but incorrect,
# ordered comparisons. In Python 2 we raise the TypeError explicitly.
if sys.version_info[0] < 3:
raise TypeError(
'unorderable types: {}() < {}()'.format(
self.__class__.__name__, other.__class__.__name__))
return NotImplemented
def __gt__(self, other):
if sys.version_info[0] < 3:
raise TypeError(
'unorderable types: {}() > {}()'.format(
self.__class__.__name__, other.__class__.__name__))
return NotImplemented
def __le__(self, other):
if sys.version_info[0] < 3:
raise TypeError(
'unorderable types: {}() <= {}()'.format(
self.__class__.__name__, other.__class__.__name__))
return NotImplemented
def __ge__(self, other):
if sys.version_info[0] < 3:
raise TypeError(
'unorderable types: {}() >= {}()'.format(
self.__class__.__name__, other.__class__.__name__))
return NotImplemented
__hash__ = object.__hash__
# Define the Enum class using metaclass syntax compatible with both Python 2
# and Python 3.
Enum = EnumMetaclass(str('Enum'), (), {
'__doc__': 'The public API Enum class.',
})
class IntEnumValue(int, EnumValue):
"""An EnumValue that is also an integer."""
def __new__(cls, enum, value, attr):
return super(IntEnumValue, cls).__new__(cls, value)
__repr__ = EnumValue.__repr__
__str__ = EnumValue.__str__
# For Python 2 (Python 3 doesn't need this to work).
__eq__ = int.__eq__
__ne__ = int.__ne__
__le__ = int.__le__
__lt__ = int.__lt__
__gt__ = int.__gt__
__ge__ = int.__ge__
# The non-deprecated version of this method.
def __int__(self):
return self._value
# For slices and index().
__index__ = __int__
class IntEnumMetaclass(EnumMetaclass):
# Define an iteration over the integer values instead of the attribute
# names.
def __iter__(cls):
for key in sorted(cls._enums):
yield getattr(cls, cls._enums[key])
IntEnum = IntEnumMetaclass(str('IntEnum'), (Enum,), {
'__doc__': 'A specialized enumeration with values that are also integers.',
'__value_factory__': IntEnumValue,
})
if str is bytes:
# Python 2
STRING_TYPE = basestring # noqa: F821
else:
# Python 3
STRING_TYPE = str
def _swap(sequence):
for key, value in sequence:
yield value, key
def _make(enum_class, name, source):
"""The common implementation for `Enum()` and `IntEnum()`."""
namespace = {}
illegals = []
have_strings = None
# Auto-splitting of strings.
if isinstance(source, STRING_TYPE):
source = source.split()
# Look for dict-like arguments. Specifically, it must have a callable
# .items() attribute. Because of the way enumerate() works, here we have
# to swap the key/values.
try:
source = _swap(source.items())
except (TypeError, AttributeError):
source = enumerate(source, start=1)
for i, item in source:
if isinstance(item, STRING_TYPE):
if have_strings is None:
have_strings = True
elif not have_strings:
raise ValueError('heterogeneous source')
namespace[item] = i
if re.match(IDENTIFIER_RE, item) is None:
illegals.append(item)
else:
if have_strings is None:
have_strings = False
elif have_strings:
raise ValueError('heterogeneous source')
item_name, item_value = item
namespace[item_name] = item_value
if re.match(IDENTIFIER_RE, item_name) is None:
illegals.append(item_name)
if len(illegals) > 0:
raise ValueError('non-identifiers: {0}'.format(SPACE.join(illegals)))
return EnumMetaclass(str(name), (enum_class,), namespace)