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

Unified Diff: Lib/enum.py

Issue 17947: Code, test, and doc review for PEP-0435 Enum
Patch Set: Created 6 years, 8 months ago
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 side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | Lib/test/test_enum.py » ('j') | Lib/test/test_enum.py » ('J')
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/enum.py Sun May 12 07:23:05 2013 -0700
@@ -0,0 +1,397 @@
+"""\
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.
+Provides the Enum class, which can be subclassed to create new, static,
+enumerations.
+"""
+
+from collections import OrderedDict
+import sys
+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.
+
+__all__ = ['Enum', 'IntEnum']
+
+
+class _StealthProperty:
+ """ 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.
+
+ A virtual attribute is one that is looked up by __getattr__, as opposed to
+ one that lives in __class__.__dict__
+
+ """
+
+ def __init__(self, fget=None):
+ self.fget = fget
+
+ def __get__(self, obj, objtype=None):
+ if obj is None:
+ raise AttributeError(self._name)
+ return self.fget(obj)
+
+ def __set__(self, obj, value):
+ raise AttributeError("can't set attribute")
+
+ def __delete__(self, obj):
+ raise AttributeError("can't delete attribute")
+
+
+class _EnumDict(dict):
+ """Keeps track of definition order of the enum items.
+
+ EnumMeta will use the names found in self._enum_names as the
+ enumeration member names.
+
+ """
+
+ def __init__(self):
+ super().__init__()
+ self._enum_names = []
+
+ def __setitem__(self, key, value):
+ """ 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.
+
+ If a descriptor is added with the same name as an enum member, the name
+ is removed from _enum_names (this may leave a hole in the numerical
+ sequence of values).
+
+ If an enum member name is used twice, but the value is not the same, an
+ 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.
+
+ """
+
+ if key[:2] == key[-2:] == '__' or hasattr(value, '__get__'):
+ if key in self._enum_names:
+ # overwriting an enum with a method? then remove the name from
+ # _enum_names or it will become an enum anyway when the class
+ # is created
+ self._enum_names.remove(key)
+ else:
+ if key in self._enum_names:
+ raise TypeError('Attempted to reuse key: %r' % key)
+ self._enum_names.append(key)
+ super().__setitem__(key, value)
+
+# dummy value for Enum as EnumMeta explicity checks for it, but of course until
+# EnumMeta finishes running the first time the Enum class doesn't exist. This
+# is also why there are checks in EnumMeta like `if Enum is not None`
+Enum = None
+
+
+class EnumMeta(type):
+ """Metaclass for Enum"""
+
+ @classmethod
+ def __prepare__(metacls, cls, bases):
+ return _EnumDict()
+
+ 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.
+ # an Enum class is final once enumeration items have been defined; it
+ # cannot be mixed with other types (int, float, etc.) if it has an
+ # inherited __new__ unless a new __new__ is defined (or the resulting
+ # class will fail).
+ obj_type, first_enum = metacls._get_mixins(bases)
+ __new__, save_new, use_args = \
+ 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
+ # save enum items into separate mapping so they don't get baked into
+ # the new class
+ name_value = {k: classdict[k] for k in classdict._enum_names}
+ for name in classdict._enum_names:
+ del classdict[name]
+ # add _name attributes to any _StealthProperty's for nicer exceptions
+ for name, value in classdict.items():
+ if isinstance(value, _StealthProperty):
+ value._name = name
+ # create our new Enum type
+ 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
+ enum_map = OrderedDict()
+ enum_names= []
+ enum_class._enum_names = enum_names # enum names in definition order
+ enum_class._enum_map = enum_map # name:value map
+ # instantiate them, checking for duplicates as we go
+ # we instantiate first instead of checking for duplicates first in case
+ # a custom __new__ is doing something funky with the values -- such as
+ # auto-numbering ;)
+ for e in classdict._enum_names:
+ value = name_value[e]
+ 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
+ args = (value, )
+ else:
+ args = value
+ if obj_type is tuple: # special case for tuple enums
+ args = (args, ) # wrap it one more time
+ if not use_args:
+ enum_item = __new__(enum_class)
+ else:
+ 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
+ enum_item.__init__(*args)
+ enum_item._name = e
+ if not hasattr(enum_item, '_value'):
+ enum_item._value = value
+ # look for any duplicate values, and, if found, use the already
+ # created enum item instead of the new one so `is` will work
+ # (i.e. Color.green is Color.grene)
+ for name, canonical_enum in enum_map.items():
+ if canonical_enum.value == enum_item._value:
+ enum_item = canonical_enum
+ break
+ else:
+ enum_names.append(e)
+ enum_map[e] = enum_item
+ # double check that repr and friends are not the mixin's or various
+ # things break (such as pickle)
+ for name in ('__repr__', '__str__', '__getnewargs__'):
+ class_method = getattr(enum_class, name)
+ obj_method = getattr(obj_type, name, None)
+ enum_method = getattr(first_enum, name, None)
+ if obj_method is not None and obj_method is class_method:
+ setattr(enum_class, name, enum_method)
+ # replace any other __new__ with our own (as long as Enum is not None,
+ # anyway) -- again, this is to support pickle
+ if Enum is not None:
+ # if the user defined their own __new__, save it before it gets
+ # clobbered in case they subclass later
+ if save_new:
+ enum_class.__new_member__ = __new__
+ enum_class.__new__ = Enum.__new__
+ return enum_class
+
+ def __call__(cls, value, names=None, *, module=None, type=None):
+ """Either returns an existing member, or creates a new enum class.
+
+ This method is used both when an enum class is given a value to match
+ to an enumeration member (i.e. Color(3)) and for the functional API
+ (i.e. Color = Enum('Color', names='red green blue')).
+
+ When used as the functional API module, if set, will be stored in the
+ new class' __module__ attribute; type, if set, will be mixed in as the
+ first base class.
+
+ Note: if module is not set this routine will attempt to discover the
+ calling module by walking the frame stack; if this is unsuccessful
+ the resulting class will not be pickleable.
+
+ """
+ if names is None: # simple value lookup
+ return cls.__new__(cls, value)
+ # otherwise, we're creating a new Enum type
+ return cls._create(value, names, module=module, type=type)
+
+ def __contains__(cls, enum_item):
+ return isinstance(enum_item, cls) and enum_item.name in cls._enum_map
+
+ def __dir__(self):
+ return ['__class__', '__doc__', '__members__'] + self._enum_names
+
+ @property
+ def __members__(cls):
+ """Returns a MappingProxyType of the internal _enum_map structure.
+
+ Do NOT return the _enum_map itself lest an innocent change by the user
+ corrupt the enum class.
+
+ """
+
+ return MappingProxyType(cls._enum_map)
+
+ def __getattr__(cls, name):
+ """Return the enum member matching `name`
+
+ We use __getattr__ instead of descriptors or inserting into the enum
+ class' __dict__ in order to support `name` and `value` being both
+ properties for enum members (which live in the class' __dict__) and
+ enum members themselves.
+
+ """
+
+ if name[:2] == name[-2:] == '__':
+ raise AttributeError(name)
+ try:
+ return cls._enum_map[name]
+ except KeyError:
+ raise AttributeError(name) from None
+
+ def __getitem__(cls, name):
+ return cls._enum_map[name]
+
+ def __iter__(cls):
+ return (cls._enum_map[name] for name in cls._enum_names)
+
+ def __len__(cls):
+ return len(cls._enum_names)
+
+ def __repr__(cls):
+ return "<enum %r>" % cls.__name__
+
+ @classmethod
+ 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.
+ """Convenience method to create a new Enum class.
+
+ Called by __new__, with the same arguments, to provide the
+ implementation. Easier to subclass this way.
+
+ """
+ metacls = cls.__class__
+ bases = (cls, ) if type is None else (type, cls)
+ classdict = metacls.__prepare__(class_name, bases)
+ if isinstance(names, str):
+ names = names.replace(',', ' ').split()
+ if isinstance(names, (tuple, list)) and isinstance(names[0], str):
+ names = [(e, i) for (i, e) in enumerate(names, 1)]
+ # otherwise names better be an iterable of (name, value) or a mapping
+ for item in names:
+ if isinstance(item, str):
+ e, v = item, names[item]
+ else:
+ e, v = item
+ classdict[e] = v
+ enum_class = metacls.__new__(metacls, class_name, bases, classdict)
+ # TODO: replace the frame hack if a blessed way to know the calling
+ # module is ever developed
+ if module is None:
+ try:
+ module = sys._getframe(1).f_globals['__name__']
+ except (AttributeError, ValueError):
+ pass
+ if module is not None:
+ enum_class.__module__ = module
+ return enum_class
+
+ @staticmethod
+ 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.
+ """Returns the type for creating enum members, and the first inherited
+ enum class.
+
+ bases: the tuple of bases that was given to __new__
+
+ """
+ if not bases:
+ return object, Enum
+ # double check that we are not subclassing a class with existing
+ # enumeration members; while we're at it, see if any other data
+ # type has been mixed in so we can use the correct __new__
+ obj_type = first_enum = None
+ for base in bases:
+ if (base is not Enum and
+ issubclass(base, Enum) and
+ base._enum_names):
+ raise TypeError("Cannot extend enumerations")
+ # base is now the last base in bases
+ if not issubclass(base, Enum):
+ raise TypeError("new enumerations must be created as "
+ "`ClassName([mixin_type,] enum_type)`")
+ # get correct mix-in type (either mix-in type of Enum subclass, or
+ # first base if last base is Enum)
+ if not issubclass(bases[0], Enum):
+ obj_type = bases[0] # first data type
+ first_enum = bases[-1] # enum type
+ else:
+ for base in bases[0].__mro__:
+ # most common: (IntEnum, int, Enum, object)
+ # possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
+ # <class 'int'>, <Enum 'Enum'>,
+ # <class 'object'>)
+ if issubclass(base, Enum):
+ if first_enum is None:
+ first_enum = base
+ else:
+ if obj_type is None:
+ obj_type = base
+ return obj_type, first_enum
+
+ @staticmethod
+ 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.
+ """Returns the __new__ to be used for creating the enum members.
+
+ classdict: the class dictionary given to __new__
+ obj_type: the data type whose __new__ will be used by default
+ first_enum: enumeration to check for an overriding __new__
+
+ """
+ # now find the correct __new__, checking to see of one was defined
+ # by the user; also check earlier enum classes in case a __new__ was
+ # saved as __new_member__
+ __new__ = classdict.get('__new__', None)
+ # should __new__ be saved as __new_member__ later?
+ save_new = __new__ is not None
+ if __new__ is None:
+ # check all possibles for __new_member__ before falling back to
+ # __new__
+ for method in ('__new_member__', '__new__'):
+ for possible in (obj_type, first_enum):
+ target = getattr(possible, method, None)
+ if target not in (None,
+ None.__new__,
+ object.__new__,
+ 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.
+ __new__ = target
+ break
+ if __new__ is not None:
+ break
+ else:
+ __new__ = object.__new__
+ # if a non-object.__new__ is used then whatever value/tuple was
+ # assigned to the enum member name will be passed to __new__ and to the
+ # new enum member's __init__
+ if __new__ is object.__new__:
+ use_args = False
+ else:
+ use_args = True
+ return __new__, save_new, use_args
+
+
+class Enum(metaclass=EnumMeta):
+ """valueless, unordered enumeration class"""
Zach Ware 2013/05/13 19:06:12 Capitalize "Valueless"
stoneleaf 2013/05/13 20:32:37 Done.
+
+ # no actual assignments are made as it is a chicken-and-egg problem
+ # with the metaclass, which checks for the Enum class specifically
+
+ def __new__(cls, value):
+ # all enum instances are actually created during class construction
+ # without calling this method; this method is called by the metaclass'
+ # __call__ (i.e. Color(3) ), and by pickle
+ if type(value) is cls:
+ return value
+ # by-value search for a matching enum member
+ for member in cls._enum_map.values():
+ if member.value == value:
+ 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
+ raise ValueError("%s is not a valid %s" % (value, cls.__name__))
+
+ def __repr__(self):
+ return "<%s.%s: %r>" % (
+ self.__class__.__name__, self._name, self._value)
+
+ def __str__(self):
+ return "%s.%s" % (self.__class__.__name__, self._name)
+
+ def __dir__(self):
+ return (['__class__', '__doc__', 'name', 'value'])
+
+ def __eq__(self, other):
+ if type(other) is self.__class__:
+ return self is other
+ return NotImplemented
+
+ def __getnewargs__(self):
+ return (self._value, )
+
+ def __hash__(self):
+ return hash(self._name)
+
+ # _StealthProperty is used to provide access to the `name` and `value`
+ # properties of enum members while keeping some measure of protection
+ # from modification, while still allowing for an enumeration to have
+ # members named `name` and `value`. This works because enumeration
+ # members are not set directely on the enum class -- __getattr__ is
+ # used to look them up.
+
+ @_StealthProperty
+ def name(self):
+ return self._name
+
+ @_StealthProperty
+ def value(self):
+ return self._value
+
+
+class IntEnum(int, Enum):
+ """Enum where members are also (and must be) ints"""
+
« 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+