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

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 """\
2 =========
eli.bendersky 2013/05/10 16:02:43 Yeah, remove this :-)
stoneleaf 2013/05/12 14:47:00 Done.
3 Copyright
4 =========
5 - Copyright: 2013 Ethan Furman -- All rights reserved.
6 - Author: Ethan Furman
7 - Version: 0.9
8 - Contact: ethan@stoneleaf.us
9
10 Permissions: See the Python Contributor Licencse Agreement.
11 """
12
13 from collections import OrderedDict
14 import sys
15
16 __all__ = ('Enum', 'IntEnum')
17
berkerpeksag 2013/05/10 16:46:19 Nit: Two blank lines. (see PEP 8 E302)
stoneleaf 2013/05/12 14:47:00 Done.
18 class StealthProperty():
berkerpeksag 2013/05/10 16:46:19 Is there a reason not to write `class StealthPrope
stoneleaf 2013/05/12 14:47:00 I just always use them.
eli.bendersky 2013/05/12 15:02:00 Remove them - they're not needed in Python 3 and t
stoneleaf 2013/05/13 20:32:37 Done.
19 """
eli.bendersky 2013/05/10 16:02:43 PEP257-ify
20 Returns the value of the instance, or the virtual attribute on the class.
21 """
22
23 # Used to provide access to the `name` and `value` properties of enum
eli.bendersky 2013/05/10 16:02:43 This class is quite generic though, and doesn't me
stoneleaf 2013/05/12 14:47:00 Done.
24 # members while keeping some measure of protection from modification,
25 # while still allowing for an enumeration to have members named `name`
26 # and `value`. This works because enumeration members are not set
27 # directely on the enum class -- __getattr__ is used to look them up.
28
29 def __init__(self, fget=None, fset=None, fdel=None, doc=None):
30 self.fget = fget
31 self.fset = fset
32 self.fdel = fdel
33 self.__doc__ = doc or fget.__doc__
34
35 def __call__(self, func):
36 self.fget = func
37 self.name = func.name
38 if not self.__doc__:
39 self.__doc__ = self.fget.__doc__
40
41 def __get__(self, obj, objtype=None):
42 if obj is None:
43 return getattr(objtype, self.name)
44 if self.fget is None:
45 raise AttributeError("unreadable attribute")
46 return self.fget(obj)
47
48 def __set__(self, obj, value):
49 if self.fset is None:
50 raise AttributeError("can't set attribute")
51 self.fset(obj, value)
52
53 def __delete__(self, obj):
54 if self.fdel is None:
55 raise AttributeError("can't delete attribute")
56 self.fdel(obj)
57
58 def setter(self, func):
59 self.fset = func
60 return self
61
62 def deleter(self, func):
63 self.fdel = func
64 return self
65
66
67 class _EnumDict(dict):
68 """Keeps track of definition order of the enum items.
69
70 EnumMeta will use the names found in self._enum_names as the
71 enumeration member names."""
72
73 def __init__(self):
74 super().__init__()
75 self._enum_names = []
76
77 def __setitem__(self, key, something):
78 """Changes anything not dundered or that doesn't have __get__.
79
80 All non-dunder names are also checked to ensure that enum items are not
81 replaced with methods, and methods are not replaced with enum items.
82
83 """
84 if key[:2] == key[-2:] == '__' or hasattr(something, '__get__'):
eli.bendersky 2013/05/10 16:02:43 parenthesize the parts of the or
stoneleaf 2013/05/12 14:47:00 like: if (key[:2] == key[-2:] == '__') or (hasatt
eli.bendersky 2013/05/12 15:02:00 Because it's easier to read. The problem is that y
85 if key in self._enum_names:
eli.bendersky 2013/05/10 16:02:43 Wait what? This also contradicts the docstring. An
stoneleaf 2013/05/12 14:47:00 Thought I had fixed the docstring. Fixed now. Mo
86 # overwriting an enum with a method?
87 self._enum_names.remove(key)
88 else:
89 if key in self._enum_names:
90 raise TypeError('Attempted to reuse key: %s' % key)
91 self._enum_names.append(key)
92 dict.__setitem__(self, key, something)
eli.bendersky 2013/05/10 16:02:43 Perhaps using super() here would be cleaner and mo
stoneleaf 2013/05/12 14:47:00 Done.
93
94 Enum = None # dummy value until replaced
eli.bendersky 2013/05/10 16:02:43 beefier comment that explains why this dummy is ne
stoneleaf 2013/05/12 14:47:00 Done.
95
96
97 class EnumMeta(type):
eli.bendersky 2013/05/10 16:02:43 Should this be _EnumMeta too?
stoneleaf 2013/05/12 14:47:00 Whatever this name is will show up in `type(Color)
98 """Metaclass for Enum
eli.bendersky 2013/05/10 16:02:43 There's no point in repeating the docs here - *how
stoneleaf 2013/05/12 14:47:00 Hmmm. This will take some thinking -- I'll come b
99
100 Pure enumerations can take any value, but the enumeration item will not
eli.bendersky 2013/05/10 16:02:43 It's not clear what a "pure" enum is
101 compare equal to its value.
102
103 Each enumeration item is created only once and is accessible as a virtual
104 attribute of its class.
105
106 Enumeration types can also be created by mixing in another data type; the
107 resulting psuedenums must all be of that mixed-in type.
eli.bendersky 2013/05/10 16:02:43 psuedenums?
108
109 """
110
111 @classmethod
112 def __prepare__(metacls, cls, bases):
113 return _EnumDict()
114
115 def __new__(metacls, cls, bases, classdict):
116 # an Enum class is final once enumeration items have been defined;
eli.bendersky 2013/05/10 16:02:43 Explain in more detail what a "data type" is
stoneleaf 2013/05/12 14:47:00 Done.
117 # it cannot be mixed with other data types if it has an inhereted
118 # __new__ unless a new __new__ is defined (or the resulting class
eli.bendersky 2013/05/10 16:02:43 Hmm, this __new__ protocol has to be documented on
stoneleaf 2013/05/12 14:47:00 It's the same __new__ you find in the normal Pytho
119 # will fail).
120 obj_type = first_enum = None
121 # double check that we are not subclassing a class with existing
122 # enumeration members; while we're at it, see if any other data
123 # type has been mixed in so we can use the correct __new__
124 if bases:
125 for base in bases:
126 if (base is not Enum
127 and issubclass(base, Enum)
eli.bendersky 2013/05/10 16:02:43 indentation
stoneleaf 2013/05/12 14:47:00 Done.
128 and base._enum_names):
129 raise TypeError("Cannot extend enumerations")
130 if not issubclass(base, Enum):
eli.bendersky 2013/05/10 16:02:43 make it more explicit that base here is the last b
stoneleaf 2013/05/12 14:47:00 Done.
131 raise TypeError("new enumerations must be created as "
132 "`ClassName([mixin_type,] enum_type)`")
133 # get correct mixin_type (either mixin type of Enum subclass,
eli.bendersky 2013/05/10 16:02:43 theres no mixin_type in the code. also close the p
stoneleaf 2013/05/12 14:47:00 Done.
134 # or first base if last base is Enum
135 if not issubclass(bases[0], Enum):
136 obj_type = bases[0] # first data type
137 first_enum = bases[-1] # enum type
138 else:
139 for base in bases[0].__mro__:
140 # most common: (IntEnum, int, Enum, object)
141 # possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
142 # <class 'int'>, <Enum 'Enum'>,
143 # <class 'object'>)
144 if issubclass(base, Enum):
145 if first_enum is None:
146 first_enum = base
147 else:
148 if obj_type is None:
149 obj_type = base
150 else:
151 obj_type = object
152 first_enum = Enum
153 # now find the correct __new__, checking to see of one was defined
154 # by the user; also check earlier enum classes in case a __new__ was
155 # saved as __new_member__
156 __new__ = classdict.get('__new__', None)
157 # should __new__ be saved as __new_member__ later?
158 save_new = __new__ is not None
159 if __new__ is None:
eli.bendersky 2013/05/10 16:02:43 if would be great to make this method shorter. hel
stoneleaf 2013/05/12 14:47:01 Done.
160 # check all possibles for __new_member__ before falling back to
161 # __new__
162 for method in ('__new_member__', '__new__'):
163 for possible in (obj_type, first_enum):
164 target = getattr(possible, method, None)
165 if target not in (None,
166 None.__new__,
167 object.__new__,
168 Enum.__new__):
169 __new__ = target
170 break
171 if __new__ is not None:
172 break
173 else:
174 __new__ = object.__new__
175 # if a non-object.__new__ is used then whatever value/tuple was
176 # assigned to the enum member name will be passed to __new__ and to the
177 # new enum member's __init__
178 if __new__ is object.__new__:
179 use_args = False
180 else:
181 use_args = True
182 # save enum items into separate mapping so they don't get baked into
183 # the new class
184 name_value = {k: classdict[k] for k in classdict._enum_names}
185 for name in classdict._enum_names:
186 del classdict[name]
187 enum_map = OrderedDict()
188 enum_class = type.__new__(metacls, cls, bases, classdict)
189 enum_names= []
190 enum_class._enum_names = enum_names # enum names in definition order
191 enum_class._enum_map = enum_map # name:value map
192 # instantiate them, checking for duplicates as we go
193 # we instantiate first instead of checking for duplicates first in case
194 # a custom __new__ is doing something funky with the values -- such as
195 # auto-numbering ;)
196 for e in classdict._enum_names:
197 value = name_value[e]
198 if not isinstance(value, tuple):
199 args = (value, )
200 else:
201 args = value
202 if obj_type is tuple: # special case for tuple enums
203 args = (args, ) # wrap it one more time
204 if not use_args:
205 enum_item = __new__(enum_class)
206 else:
207 enum_item = __new__(enum_class, *args)
208 enum_item.__init__(*args)
209 enum_item._name = e
210 if not hasattr(enum_item, '_value'):
211 enum_item._value = value
212 # look for any duplicate values, and, if found, use the already
213 # created enum item instead of the new one so `is` will work
214 # (i.e. Color.green is Color.grene)
215 for name, canonical_enum in enum_map.items():
216 if canonical_enum.value == enum_item._value:
217 enum_item = canonical_enum
218 break
219 else:
220 enum_names.append(e)
221 enum_map[e] = enum_item
222 # double check that repr and friends are not the mixin's or various
223 # things break (such as pickle)
224 for name in ('__repr__', '__str__', '__getnewargs__'):
225 class_method = getattr(enum_class, name)
226 obj_method = getattr(obj_type, name, None)
227 enum_method = getattr(first_enum, name, None)
228 if obj_method is not None and obj_method is class_method:
229 setattr(enum_class, name, enum_method)
230 # replace any other __new__ with our own (as long as Enum is not None,
231 # anyway) -- again, this is to support pickle
232 if Enum is not None:
233 # if the user defined their own __new__, save it before it gets
234 # clobbered in case they subclass later
235 if save_new:
236 enum_class.__new_member__ = __new__
237 enum_class.__new__ = Enum.__new__
238 return enum_class
239
240 def __call__(cls, value, names=None, *, module_name=None, type=None):
eli.bendersky 2013/05/10 16:02:43 Hmm, PEP 435 says "module" not "module_name". Whil
stoneleaf 2013/05/12 14:47:01 Changed back to `module`.
241 if names is None: # simple value lookup
242 return cls.__new__(cls, value)
243 # otherwise, we're creating a new Enum type
244 class_name = value # better name for a name than value ;)
eli.bendersky 2013/05/10 16:02:43 I'd suggest a new method here: _make_enum_from_na
stoneleaf 2013/05/12 14:47:01 Not much happens before this point, and everything
stoneleaf 2013/05/12 14:49:55 Okay, it buys us better subclassing support. Made
245 metacls = cls.__class__
246 bases = (cls, ) if type is None else (type, cls)
247 classdict = metacls.__prepare__(class_name, bases)
248 if isinstance(names, str):
249 names = names.replace(',', ' ').split()
250 if isinstance(names, (tuple, list)) and isinstance(names[0], str):
251 names = [(e, i) for (i, e) in enumerate(names, 1)]
252 # otherwise names better be an iterable of (name, value) or a mapping
253 for item in names:
254 if isinstance(item, str):
255 e, v = item, names[item]
256 else:
257 e, v = item
258 classdict[e] = v
259 enum_class = metacls.__new__(metacls, class_name, bases, classdict)
260 # TODO: replace the frame hack if a blessed way to know the calling
261 # module is ever developed
262 if module_name is None:
263 try:
264 module_name = sys._getframe(1).f_globals['__name__']
265 except (AttributeError, ValueError):
266 pass
267 if module_name is not None:
268 enum_class.__module__ = module_name
269 return enum_class
270
271 def __contains__(cls, enum_item):
272 return isinstance(enum_item, cls) and enum_item.name in cls._enum_map
273
274 def __dir__(self):
275 return (['__class__', '__doc__', '__members__']
276 + list(self.__members__))
277
278 @property
279 def __members__(cls):
280 return cls._enum_map.copy()
eli.bendersky 2013/05/10 16:02:43 why copy?
stoneleaf 2013/05/12 14:47:01 If we return the actual map, and it gets modified,
eli.bendersky 2013/05/12 15:02:00 That's tough life, no doubt ;-), but appropriate i
stoneleaf 2013/05/13 20:32:37 That's not actually correct. We don't do many, bu
281
282 def __getattr__(cls, name):
283 """Return the enum item matching `name`"""
284 if name[:2] == name[-2:] == '__':
285 raise AttributeError(name)
286 try:
287 return cls._enum_map[name]
288 except KeyError:
289 raise AttributeError(name) from None
290
291 def __getitem__(cls, name):
292 return cls._enum_map[name]
293
294 def __iter__(cls):
295 return (cls._enum_map[name] for name in cls._enum_names)
296
297 def __len__(cls):
298 return len(cls._enum_names)
299
300 def __repr__(cls):
301 return "<enum %r>" % cls.__name__
302
303
304 class Enum(metaclass=EnumMeta):
305 """valueless, unordered enumeration class"""
306
307 # no actual assignments are made as it is a chicken-and-egg problem
308 # with the metaclass, which checks for the Enum class specifically
309
310 def __new__(cls, value):
311 # all enum instances are actually created during class construction
312 # without calling this method; this method is called by the metaclass'
313 # __call__ (i.e. Color(3) ), and by pickle
314 if type(value) is cls:
315 return value
316 # by-value search for a matching enum member
317 for enum_name in cls._enum_names:
318 enum_member = cls._enum_map[enum_name]
eli.bendersky 2013/05/10 16:02:43 why don't you just search enum_map directly?
stoneleaf 2013/05/12 14:47:01 Done.
319 if enum_member.value == value:
320 return enum_member
321 raise ValueError("%s is not a valid %s" % (value, cls.__name__))
322
323 def __repr__(self):
324 return "<%s.%s: %r>" % (
325 self.__class__.__name__, self._name, self._value)
326
327 def __str__(self):
328 return "%s.%s" % (self.__class__.__name__, self._name)
329
330 def __dir__(self):
331 return (['__class__', '__doc__', 'name', 'value'])
332
333 def __eq__(self, other):
334 if type(other) is self.__class__:
335 return self is other
336 return NotImplemented
337
338 def __getnewargs__(self):
339 return (self._value, )
340
341 def __hash__(self):
342 return hash(self._name)
343
344 @StealthProperty
eli.bendersky 2013/05/10 16:02:43 OK, here a brief explanation of how _StealthProper
stoneleaf 2013/05/12 14:47:01 Done.
345 def name(self):
346 return self._name
347
348 @StealthProperty
349 def value(self):
350 return self._value
351
352
353 class IntEnum(int, Enum):
354 """Enum where members are also (and must be) ints"""
355
356 if __name__ == '__main__':
eli.bendersky 2013/05/10 16:02:43 remove this
stoneleaf 2013/05/12 14:47:01 Done.
357 print("that's all, folks!")
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+