Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(169638)

Side by Side Diff: Lib/enum.py

Issue 17947: Code, test, and doc review for PEP-0435 Enum
Patch Set: Created 6 years, 7 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | Lib/test/test_enum.py » ('j') | Lib/test/test_enum.py » ('J')
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 """\
Zach Ware 2013/05/13 19:06:12 It's ok to lose the backslash here, but start the
stoneleaf 2013/05/13 20:32:37 Done.
2 Provides the Enum class, which can be subclassed to create new, static,
3 enumerations.
4 """
5
6 from collections import OrderedDict
7 import sys
8 from types import MappingProxyType
Zach Ware 2013/05/13 19:06:12 It looks better to order by 'import ...', then 'fr
stoneleaf 2013/05/13 20:32:37 Done.
9
10 __all__ = ['Enum', 'IntEnum']
11
12
13 class _StealthProperty:
14 """ Returns the value in the instance, raises AtttributeError on the class.
Zach Ware 2013/05/13 19:06:12 Missed a space before Returns.
isoschiz 2013/05/13 19:25:49 (minor) typo in "AtttributeError"
stoneleaf 2013/05/13 20:32:37 Done.
stoneleaf 2013/05/13 20:32:37 Done.
15
16 A virtual attribute is one that is looked up by __getattr__, as opposed to
17 one that lives in __class__.__dict__
18
19 """
20
21 def __init__(self, fget=None):
22 self.fget = fget
23
24 def __get__(self, obj, objtype=None):
25 if obj is None:
26 raise AttributeError(self._name)
27 return self.fget(obj)
28
29 def __set__(self, obj, value):
30 raise AttributeError("can't set attribute")
31
32 def __delete__(self, obj):
33 raise AttributeError("can't delete attribute")
34
35
36 class _EnumDict(dict):
37 """Keeps track of definition order of the enum items.
38
39 EnumMeta will use the names found in self._enum_names as the
40 enumeration member names.
41
42 """
43
44 def __init__(self):
45 super().__init__()
46 self._enum_names = []
47
48 def __setitem__(self, key, value):
49 """ Changes anything not dundered or that doesn't have __get__.
Zach Ware 2013/05/13 19:06:12 Missed another space.
stoneleaf 2013/05/13 20:32:37 Done.
50
51 If a descriptor is added with the same name as an enum member, the name
52 is removed from _enum_names (this may leave a hole in the numerical
53 sequence of values).
54
55 If an enum member name is used twice, but the value is not the same, an
56 error is raised.
isoschiz 2013/05/13 19:25:49 This sentence is not true (though it might be nice
stoneleaf 2013/05/13 20:32:37 Forgot to update the docstring. Done.
57
58 """
59
60 if key[:2] == key[-2:] == '__' or hasattr(value, '__get__'):
61 if key in self._enum_names:
62 # overwriting an enum with a method? then remove the name from
63 # _enum_names or it will become an enum anyway when the class
64 # is created
65 self._enum_names.remove(key)
66 else:
67 if key in self._enum_names:
68 raise TypeError('Attempted to reuse key: %r' % key)
69 self._enum_names.append(key)
70 super().__setitem__(key, value)
71
72 # dummy value for Enum as EnumMeta explicity checks for it, but of course until
73 # EnumMeta finishes running the first time the Enum class doesn't exist. This
74 # is also why there are checks in EnumMeta like `if Enum is not None`
75 Enum = None
76
77
78 class EnumMeta(type):
79 """Metaclass for Enum"""
80
81 @classmethod
82 def __prepare__(metacls, cls, bases):
83 return _EnumDict()
84
85 def __new__(metacls, cls, bases, classdict):
Zach Ware 2013/05/13 19:06:12 This method really just looks like a giant wall of
stoneleaf 2013/05/13 20:32:37 I was wondering if I could add some blank lines.
86 # an Enum class is final once enumeration items have been defined; it
87 # cannot be mixed with other types (int, float, etc.) if it has an
88 # inherited __new__ unless a new __new__ is defined (or the resulting
89 # class will fail).
90 obj_type, first_enum = metacls._get_mixins(bases)
91 __new__, save_new, use_args = \
92 metacls._find_new(classdict, obj_type, first_enum)
Zach Ware 2013/05/13 19:06:12 PEP 8 recommends avoiding backslashes for line con
stoneleaf 2013/05/13 20:32:37 How about: """ __new__, save_new, use_arg
Zach Ware 2013/05/17 15:37:11 That's not my preference, but I suppose it's bette
93 # save enum items into separate mapping so they don't get baked into
94 # the new class
95 name_value = {k: classdict[k] for k in classdict._enum_names}
96 for name in classdict._enum_names:
97 del classdict[name]
98 # add _name attributes to any _StealthProperty's for nicer exceptions
99 for name, value in classdict.items():
100 if isinstance(value, _StealthProperty):
101 value._name = name
102 # create our new Enum type
103 enum_class = type.__new__(metacls, cls, bases, classdict)
isoschiz 2013/05/13 19:25:49 Shouldn't this technically be a use of super()? No
stoneleaf 2013/05/13 20:32:37 __new__ is one of the few things (the only thing?)
isoschiz 2013/05/14 00:01:06 And yet in this instance you are deferring to your
stoneleaf 2013/05/14 00:09:15 I don't understand. Can you provide an example?
isoschiz 2013/05/14 01:29:56 It requires writing an extension type (i.e. a type
stoneleaf 2013/05/14 01:41:45 I think you are confusing the metaclass __new__ wi
isoschiz 2013/05/14 20:12:43 Sorry - I did typo: I meant MyEnumMeta, as you ind
stoneleaf 2013/05/14 20:18:48 One of us is confused -- and I don't know which of
isoschiz 2013/05/14 20:40:09 Because in all cases we pass metacls up as the fir
stoneleaf 2013/05/14 21:04:02 Ah, now I understand. Thank you for taking the ti
104 enum_map = OrderedDict()
105 enum_names= []
106 enum_class._enum_names = enum_names # enum names in definition order
107 enum_class._enum_map = enum_map # name:value map
108 # instantiate them, checking for duplicates as we go
109 # we instantiate first instead of checking for duplicates first in case
110 # a custom __new__ is doing something funky with the values -- such as
111 # auto-numbering ;)
112 for e in classdict._enum_names:
113 value = name_value[e]
114 if not isinstance(value, tuple):
isoschiz 2013/05/13 19:25:49 Doesn't this produce weird behaviour for the follo
stoneleaf 2013/05/13 20:32:37 Did you try it? """ --> class MyEnum(Enum): FOO =
isoschiz 2013/05/14 00:01:06 I must confess I didn't try it - apologies. :-) A
stoneleaf 2013/05/14 00:09:15 Probably because it doesn't work straight-forwardl
115 args = (value, )
116 else:
117 args = value
118 if obj_type is tuple: # special case for tuple enums
119 args = (args, ) # wrap it one more time
120 if not use_args:
121 enum_item = __new__(enum_class)
122 else:
123 enum_item = __new__(enum_class, *args)
isoschiz 2013/05/13 19:25:49 Is it explicitly forbidden to have multi type enum
stoneleaf 2013/05/13 20:32:37 If it's a mixed enum (like IntEnum) all the values
124 enum_item.__init__(*args)
125 enum_item._name = e
126 if not hasattr(enum_item, '_value'):
127 enum_item._value = value
128 # look for any duplicate values, and, if found, use the already
129 # created enum item instead of the new one so `is` will work
130 # (i.e. Color.green is Color.grene)
131 for name, canonical_enum in enum_map.items():
132 if canonical_enum.value == enum_item._value:
133 enum_item = canonical_enum
134 break
135 else:
136 enum_names.append(e)
137 enum_map[e] = enum_item
138 # double check that repr and friends are not the mixin's or various
139 # things break (such as pickle)
140 for name in ('__repr__', '__str__', '__getnewargs__'):
141 class_method = getattr(enum_class, name)
142 obj_method = getattr(obj_type, name, None)
143 enum_method = getattr(first_enum, name, None)
144 if obj_method is not None and obj_method is class_method:
145 setattr(enum_class, name, enum_method)
146 # replace any other __new__ with our own (as long as Enum is not None,
147 # anyway) -- again, this is to support pickle
148 if Enum is not None:
149 # if the user defined their own __new__, save it before it gets
150 # clobbered in case they subclass later
151 if save_new:
152 enum_class.__new_member__ = __new__
153 enum_class.__new__ = Enum.__new__
154 return enum_class
155
156 def __call__(cls, value, names=None, *, module=None, type=None):
157 """Either returns an existing member, or creates a new enum class.
158
159 This method is used both when an enum class is given a value to match
160 to an enumeration member (i.e. Color(3)) and for the functional API
161 (i.e. Color = Enum('Color', names='red green blue')).
162
163 When used as the functional API module, if set, will be stored in the
164 new class' __module__ attribute; type, if set, will be mixed in as the
165 first base class.
166
167 Note: if module is not set this routine will attempt to discover the
168 calling module by walking the frame stack; if this is unsuccessful
169 the resulting class will not be pickleable.
170
171 """
172 if names is None: # simple value lookup
173 return cls.__new__(cls, value)
174 # otherwise, we're creating a new Enum type
175 return cls._create(value, names, module=module, type=type)
176
177 def __contains__(cls, enum_item):
178 return isinstance(enum_item, cls) and enum_item.name in cls._enum_map
179
180 def __dir__(self):
181 return ['__class__', '__doc__', '__members__'] + self._enum_names
182
183 @property
184 def __members__(cls):
185 """Returns a MappingProxyType of the internal _enum_map structure.
186
187 Do NOT return the _enum_map itself lest an innocent change by the user
188 corrupt the enum class.
189
190 """
191
192 return MappingProxyType(cls._enum_map)
193
194 def __getattr__(cls, name):
195 """Return the enum member matching `name`
196
197 We use __getattr__ instead of descriptors or inserting into the enum
198 class' __dict__ in order to support `name` and `value` being both
199 properties for enum members (which live in the class' __dict__) and
200 enum members themselves.
201
202 """
203
204 if name[:2] == name[-2:] == '__':
205 raise AttributeError(name)
206 try:
207 return cls._enum_map[name]
208 except KeyError:
209 raise AttributeError(name) from None
210
211 def __getitem__(cls, name):
212 return cls._enum_map[name]
213
214 def __iter__(cls):
215 return (cls._enum_map[name] for name in cls._enum_names)
216
217 def __len__(cls):
218 return len(cls._enum_names)
219
220 def __repr__(cls):
221 return "<enum %r>" % cls.__name__
222
223 @classmethod
224 def _create(cls, class_name, names=None, *, module=None, type=None):
Zach Ware 2013/05/13 19:06:12 This one could also do with a touch of whitespace
stoneleaf 2013/05/13 20:32:37 Done.
225 """Convenience method to create a new Enum class.
226
227 Called by __new__, with the same arguments, to provide the
228 implementation. Easier to subclass this way.
229
230 """
231 metacls = cls.__class__
232 bases = (cls, ) if type is None else (type, cls)
233 classdict = metacls.__prepare__(class_name, bases)
234 if isinstance(names, str):
235 names = names.replace(',', ' ').split()
236 if isinstance(names, (tuple, list)) and isinstance(names[0], str):
237 names = [(e, i) for (i, e) in enumerate(names, 1)]
238 # otherwise names better be an iterable of (name, value) or a mapping
239 for item in names:
240 if isinstance(item, str):
241 e, v = item, names[item]
242 else:
243 e, v = item
244 classdict[e] = v
245 enum_class = metacls.__new__(metacls, class_name, bases, classdict)
246 # TODO: replace the frame hack if a blessed way to know the calling
247 # module is ever developed
248 if module is None:
249 try:
250 module = sys._getframe(1).f_globals['__name__']
251 except (AttributeError, ValueError):
252 pass
253 if module is not None:
254 enum_class.__module__ = module
255 return enum_class
256
257 @staticmethod
258 def _get_mixins(bases):
Zach Ware 2013/05/13 19:06:12 Whitespace wouldn't be amiss.
stoneleaf 2013/05/13 20:32:37 Done.
259 """Returns the type for creating enum members, and the first inherited
260 enum class.
261
262 bases: the tuple of bases that was given to __new__
263
264 """
265 if not bases:
266 return object, Enum
267 # double check that we are not subclassing a class with existing
268 # enumeration members; while we're at it, see if any other data
269 # type has been mixed in so we can use the correct __new__
270 obj_type = first_enum = None
271 for base in bases:
272 if (base is not Enum and
273 issubclass(base, Enum) and
274 base._enum_names):
275 raise TypeError("Cannot extend enumerations")
276 # base is now the last base in bases
277 if not issubclass(base, Enum):
278 raise TypeError("new enumerations must be created as "
279 "`ClassName([mixin_type,] enum_type)`")
280 # get correct mix-in type (either mix-in type of Enum subclass, or
281 # first base if last base is Enum)
282 if not issubclass(bases[0], Enum):
283 obj_type = bases[0] # first data type
284 first_enum = bases[-1] # enum type
285 else:
286 for base in bases[0].__mro__:
287 # most common: (IntEnum, int, Enum, object)
288 # possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
289 # <class 'int'>, <Enum 'Enum'>,
290 # <class 'object'>)
291 if issubclass(base, Enum):
292 if first_enum is None:
293 first_enum = base
294 else:
295 if obj_type is None:
296 obj_type = base
297 return obj_type, first_enum
298
299 @staticmethod
300 def _find_new(classdict, obj_type, first_enum):
Zach Ware 2013/05/13 19:06:12 Another whitespace request.
stoneleaf 2013/05/13 20:32:37 Done.
301 """Returns the __new__ to be used for creating the enum members.
302
303 classdict: the class dictionary given to __new__
304 obj_type: the data type whose __new__ will be used by default
305 first_enum: enumeration to check for an overriding __new__
306
307 """
308 # now find the correct __new__, checking to see of one was defined
309 # by the user; also check earlier enum classes in case a __new__ was
310 # saved as __new_member__
311 __new__ = classdict.get('__new__', None)
312 # should __new__ be saved as __new_member__ later?
313 save_new = __new__ is not None
314 if __new__ is None:
315 # check all possibles for __new_member__ before falling back to
316 # __new__
317 for method in ('__new_member__', '__new__'):
318 for possible in (obj_type, first_enum):
319 target = getattr(possible, method, None)
320 if target not in (None,
321 None.__new__,
322 object.__new__,
323 Enum.__new__):
isoschiz 2013/05/13 19:25:49 This should be a set literal, not a tuple.
stoneleaf 2013/05/13 20:32:37 Done.
324 __new__ = target
325 break
326 if __new__ is not None:
327 break
328 else:
329 __new__ = object.__new__
330 # if a non-object.__new__ is used then whatever value/tuple was
331 # assigned to the enum member name will be passed to __new__ and to the
332 # new enum member's __init__
333 if __new__ is object.__new__:
334 use_args = False
335 else:
336 use_args = True
337 return __new__, save_new, use_args
338
339
340 class Enum(metaclass=EnumMeta):
341 """valueless, unordered enumeration class"""
Zach Ware 2013/05/13 19:06:12 Capitalize "Valueless"
stoneleaf 2013/05/13 20:32:37 Done.
342
343 # no actual assignments are made as it is a chicken-and-egg problem
344 # with the metaclass, which checks for the Enum class specifically
345
346 def __new__(cls, value):
347 # all enum instances are actually created during class construction
348 # without calling this method; this method is called by the metaclass'
349 # __call__ (i.e. Color(3) ), and by pickle
350 if type(value) is cls:
351 return value
352 # by-value search for a matching enum member
353 for member in cls._enum_map.values():
354 if member.value == value:
355 return member
alex 2013/05/13 19:35:53 I would have assumed that that creating an enum fr
stoneleaf 2013/05/13 20:32:37 Sure, but it would add a bunch of complexity, and
alex 2013/05/13 20:34:50 Why do you think it's a one time operation? Everyt
stoneleaf 2013/05/13 20:39:24 Sorry, saw the __new__ and thought you were talkin
356 raise ValueError("%s is not a valid %s" % (value, cls.__name__))
357
358 def __repr__(self):
359 return "<%s.%s: %r>" % (
360 self.__class__.__name__, self._name, self._value)
361
362 def __str__(self):
363 return "%s.%s" % (self.__class__.__name__, self._name)
364
365 def __dir__(self):
366 return (['__class__', '__doc__', 'name', 'value'])
367
368 def __eq__(self, other):
369 if type(other) is self.__class__:
370 return self is other
371 return NotImplemented
372
373 def __getnewargs__(self):
374 return (self._value, )
375
376 def __hash__(self):
377 return hash(self._name)
378
379 # _StealthProperty is used to provide access to the `name` and `value`
380 # properties of enum members while keeping some measure of protection
381 # from modification, while still allowing for an enumeration to have
382 # members named `name` and `value`. This works because enumeration
383 # members are not set directely on the enum class -- __getattr__ is
384 # used to look them up.
385
386 @_StealthProperty
387 def name(self):
388 return self._name
389
390 @_StealthProperty
391 def value(self):
392 return self._value
393
394
395 class IntEnum(int, Enum):
396 """Enum where members are also (and must be) ints"""
397
OLDNEW
« no previous file with comments | « no previous file | Lib/test/test_enum.py » ('j') | Lib/test/test_enum.py » ('J')

RSS Feeds Recent Issues | This issue
This is Rietveld 894c83f36cb7+