diff -r 974d4844d5a7 Doc/library/enum.rst --- a/Doc/library/enum.rst Wed Jun 19 09:01:58 2013 -0700 +++ b/Doc/library/enum.rst Sat Jun 29 11:22:39 2013 -0700 @@ -11,21 +11,24 @@ **Source code:** :source:`Lib/enum.py` ---------------- An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over. This module defines two enumeration classes that can be used to define unique -sets of names and values: :class:`Enum` and :class:`IntEnum`. +sets of names and values: :class:`Enum` and :class:`IntEnum`. It also defines +one decorator, :func:`unique`, that ensures only unique member names are +present in an enumeration. + Creating an Enum ---------------- Enumerations are created using the :keyword:`class` syntax, which makes them easy to read and write. An alternative creation method is described in `Functional API`_. To define an enumeration, subclass :class:`Enum` as follows:: >>> from enum import Enum @@ -80,39 +83,47 @@ Enumerations support iteration, in defin Enumeration members are hashable, so they can be used in dictionaries and sets:: >>> apples = {} >>> apples[Color.red] = 'red delicious' >>> apples[Color.green] = 'granny smith' >>> apples == {Color.red: 'red delicious', Color.green: 'granny smith'} True -Programmatic access to enumeration members ------------------------------------------- +Programmatic access to enumeration members and their attributes +--------------------------------------------------------------- Sometimes it's useful to access members in enumerations programmatically (i.e. situations where ``Color.red`` won't do because the exact color is not known at program-writing time). ``Enum`` allows such access:: >>> Color(1) >>> Color(3) If you want to access enum members by *name*, use item access:: >>> Color['red'] >>> Color['green'] +If have an enum member and need its :attr:`name` or :attr:`value`:: + + >>> member = Color.red + >>> member.name + 'red' + >>> member.value + 1 + Duplicating enum members and values ----------------------------------- Having two enum members with the same name is invalid:: >>> class Shape(Enum): ... square = 2 ... square = 3 ... @@ -131,20 +142,49 @@ return A:: ... circle = 3 ... alias_for_square = 2 ... >>> Shape.square >>> Shape.alias_for_square >>> Shape(2) + +Ensuring unique enumeration values +================================== + +By default, enumerations allow multiple names as aliases for the same value. +When this behaviour isn't desired, the following decorator can be used to +ensure each value is used only once in the enumeration: + +.. decorator:: unique + +A :keyword:`class` decorator specifically for enumerations. It searches an +enumeration's :attr:`__members__` gathering any aliases it finds; if any are +found :exc:`ValueError` is raised with the details:: + + >>> from enum import Enum, unique + >>> @unique + ... class Mistake(Enum): + ... one = 1 + ... two = 2 + ... three = 3 + ... four = 3 + Traceback (most recent call last): + ... + ValueError: duplicate values found in : four -> three + + +Iteration +========= + Iterating over the members of an enum does not provide the aliases:: >>> list(Shape) [, , ] The special attribute ``__members__`` is an ordered dictionary mapping names to members. It includes all names defined in the enumeration, including the aliases:: >>> for name, member in Shape.__members__.items(): diff -r 974d4844d5a7 Lib/enum.py --- a/Lib/enum.py Wed Jun 19 09:01:58 2013 -0700 +++ b/Lib/enum.py Sat Jun 29 11:22:39 2013 -0700 @@ -1,17 +1,17 @@ """Python Enumerations""" import sys from collections import OrderedDict from types import MappingProxyType -__all__ = ['Enum', 'IntEnum'] +__all__ = ['Enum', 'IntEnum', 'unique'] class _RouteClassAttributeToGetattr: """Route attribute access on a class to __getattr__. This is a descriptor, used to define attributes that act differently when accessed through an instance and through a class. Instance access remains normal, but access to an attribute through a class will be routed to the class's __getattr__ method; this is done by raising AttributeError. @@ -456,10 +456,26 @@ class Enum(metaclass=EnumMeta): def name(self): return self._name @_RouteClassAttributeToGetattr def value(self): return self._value class IntEnum(int, Enum): """Enum where members are also (and must be) ints""" + + +def unique(enumeration): + """Class decorator that ensures only unique members exist in an enumeration.""" + duplicates = [] + for name, member in enumeration.__members__.items(): + if name != member.name: + duplicates.append((name, member.name)) + if duplicates: + alias_details = ', '.join( + ["%s -> %s" % (alias, name) for (alias, name) in duplicates] + ) + raise ValueError('duplicate values found in %r: %s' % + (enumeration, alias_details) + ) + return enumeration diff -r 974d4844d5a7 Lib/test/test_enum.py --- a/Lib/test/test_enum.py Wed Jun 19 09:01:58 2013 -0700 +++ b/Lib/test/test_enum.py Sat Jun 29 11:22:39 2013 -0700 @@ -1,15 +1,15 @@ import enum import unittest from collections import OrderedDict from pickle import dumps, loads, PicklingError -from enum import Enum, IntEnum +from enum import Enum, IntEnum, unique # for pickle tests try: class Stooges(Enum): LARRY = 1 CURLY = 2 MOE = 3 except Exception as exc: Stooges = exc @@ -910,12 +910,42 @@ class TestEnum(unittest.TestCase): self.radius = radius # in meters @property def surface_gravity(self): # universal gravitational constant (m3 kg-1 s-2) G = 6.67300E-11 return G * self.mass / (self.radius * self.radius) self.assertEqual(round(Planet.EARTH.surface_gravity, 2), 9.80) self.assertEqual(Planet.EARTH.value, (5.976e+24, 6.37814e6)) +class TestUnique(unittest.TestCase): + + def test_unique_clean(self): + @unique + class Clean(Enum): + one = 1 + two = 'dos' + tres = 4.0 + @unique + class Cleaner(IntEnum): + single = 1 + double = 2 + triple = 3 + + def test_unique_dirty(self): + with self.assertRaisesRegex(ValueError, 'tres.*one'): + @unique + class Dirty(Enum): + one = 1 + two = 'dos' + tres = 1 + with self.assertRaisesRegex(ValueError, 'double.*single.*turkey.*triple'): + @unique + class Dirtier(IntEnum): + single = 1 + double = 1 + triple = 3 + turkey = 3 + + if __name__ == '__main__': unittest.main()