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

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') | no next file with comments »
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 Fri May 10 08:19:27 2013 -0700
@@ -0,0 +1,363 @@
+"""\
+Provides the Enum class, which can be subclassed to create new, static, enumerations.
ezio.melotti 2013/05/10 20:18:59 Line too long.
stoneleaf 2013/05/12 14:47:01 Done.
+"""
+
+from collections import OrderedDict
+import sys
+
+__all__ = ('Enum', 'IntEnum')
eric.araujo 2013/05/10 18:30:30 Some tools (pydoc?) have issues if __all__ is not
stoneleaf 2013/05/12 14:47:01 Done.
+
+class _StealthProperty():
+ """
+ Returns the value in the instance, or the virtual attribute on the class.
+
+ 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, fset=None, fdel=None, doc=None):
+ self.fget = fget
+ self.fset = fset
+ self.fdel = fdel
+ self.__doc__ = doc or fget.__doc__
+
+ def __call__(self, func):
+ self.fget = func
+ self.name = func.name
+ if not self.__doc__:
+ self.__doc__ = self.fget.__doc__
+
+ def __get__(self, obj, objtype=None):
+ if obj is None:
+ return getattr(objtype, self.name)
+ if self.fget is None:
+ raise AttributeError("unreadable attribute")
+ return self.fget(obj)
+
+ def __set__(self, obj, value):
+ if self.fset is None:
+ raise AttributeError("can't set attribute")
+ self.fset(obj, value)
+
+ def __delete__(self, obj):
+ if self.fdel is None:
+ raise AttributeError("can't delete attribute")
+ self.fdel(obj)
+
+ def setter(self, func):
+ self.fset = func
+ return self
+
+ def deleter(self, func):
+ self.fdel = func
+ return self
+
+
+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, something):
+ """\
+ Changes anything not dundered or that doesn't have __get__.
+
+ 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.
+
+ """
+ if key[:2] == key[-2:] == '__' or hasattr(something, '__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 and self[key] != something:
ezio.melotti 2013/05/10 20:18:59 Does this mean than defining the same element twic
stoneleaf 2013/05/12 14:47:01 Probably not, took it back out.
+ raise TypeError('Attempted to reuse key: %s' % key)
ezio.melotti 2013/05/10 20:18:59 Using %r is probably better.
stoneleaf 2013/05/12 14:47:01 Done.
+ self._enum_names.append(key)
+ super().__setitem__(key, something)
+
+# 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):
+ # 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)
+ # 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]
+ enum_map = OrderedDict()
+ enum_class = type.__new__(metacls, cls, bases, classdict)
+ 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):
+ 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)
+ 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):
+ if names is None: # simple value lookup
+ return cls.__new__(cls, value)
+ # otherwise, we're creating a new Enum type
+ class_name = value # better name for a name than value ;)
+ 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
+
+ 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__']
+ + list(self.__members__))
ezio.melotti 2013/05/10 20:18:59 The operator should go at the end of the previous
stoneleaf 2013/05/12 14:47:01 Done.
+
+ @property
+ def __members__(cls):
+ return cls._enum_map.copy()
+
+ def __getattr__(cls, name):
+ """Return the enum item matching `name`"""
+ 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__
+
+ @staticmethod
+ def _get_mixins(bases):
+ obj_type = first_enum = None
+ # 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__
+ if bases:
+ for base in bases:
+ if (base is not Enum
+ and issubclass(base, Enum)
+ and base._enum_names):
ezio.melotti 2013/05/10 20:18:59 The operator should go at the end of the previous
stoneleaf 2013/05/12 14:47:01 Done.
+ 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
+ else:
+ obj_type = object
+ first_enum = Enum
+ return obj_type, first_enum
ezio.melotti 2013/05/10 20:18:59 You could simplify this a bit and save some indent
stoneleaf 2013/05/12 14:47:01 Good call. Done. (Couldn't do it at 252, though.
+
+ @staticmethod
+ def _find_new(classdict, obj_type, first_enum):
+ # 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):
ezio.melotti 2013/05/10 20:18:59 You could use sets here.
stoneleaf 2013/05/12 14:47:01 I'm not sure how -- example?
+ target = getattr(possible, method, None)
+ if target not in (None,
+ None.__new__,
+ object.__new__,
+ Enum.__new__):
+ __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"""
+
+ # 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
+ 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') | no next file with comments »

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