]> granicus.if.org Git - python/commitdiff
closes issue18042 -- a `unique` decorator is added to enum.py
authorEthan Furman <ethan@stoneleaf.us>
Fri, 19 Jul 2013 00:05:39 +0000 (17:05 -0700)
committerEthan Furman <ethan@stoneleaf.us>
Fri, 19 Jul 2013 00:05:39 +0000 (17:05 -0700)
The docs also clarify the 'Interesting Example' duplicate-free enum is for
demonstration purposes.

Doc/library/enum.rst
Lib/enum.py
Lib/test/test_enum.py

index b919bdc83970c838b4fdbf56caad61e27be51ff2..1e464d7361df7a25f4774aceb02daecb084bc4ff 100644 (file)
@@ -18,7 +18,10 @@ 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 values are
+present in an enumeration.
+
 
 Creating an Enum
 ----------------
@@ -146,6 +149,35 @@ return A::
     >>> Shape(2)
     <Shape.square: 2>
 
+
+Ensuring unique enumeration values
+==================================
+
+By default, enumerations allow multiple names as aliases for the same value.
+When this behavior 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 <enum 'Mistake'>: four -> three
+
+
+Iteration
+=========
+
 Iterating over the members of an enum does not provide the aliases::
 
     >>> list(Shape)
@@ -169,6 +201,7 @@ the enumeration members.  For example, finding all the aliases::
     >>> [name for name, member in Shape.__members__.items() if member.name != name]
     ['alias_for_square']
 
+
 Comparisons
 -----------
 
@@ -462,32 +495,6 @@ Avoids having to specify the value for each enumeration member::
     True
 
 
-UniqueEnum
-----------
-
-Raises an error if a duplicate member name is found instead of creating an
-alias::
-
-    >>> class UniqueEnum(Enum):
-    ...     def __init__(self, *args):
-    ...         cls = self.__class__
-    ...         if any(self.value == e.value for e in cls):
-    ...             a = self.name
-    ...             e = cls(self.value).name
-    ...             raise ValueError(
-    ...                     "aliases not allowed in UniqueEnum:  %r --> %r"
-    ...                     % (a, e))
-    ...
-    >>> class Color(UniqueEnum):
-    ...     red = 1
-    ...     green = 2
-    ...     blue = 3
-    ...     grene = 2
-    Traceback (most recent call last):
-    ...
-    ValueError: aliases not allowed in UniqueEnum:  'grene' --> 'green'
-
-
 OrderedEnum
 -----------
 
@@ -524,6 +531,38 @@ enumerations)::
     True
 
 
+DuplicateFreeEnum
+-----------------
+
+Raises an error if a duplicate member name is found instead of creating an
+alias::
+
+    >>> class DuplicateFreeEnum(Enum):
+    ...     def __init__(self, *args):
+    ...         cls = self.__class__
+    ...         if any(self.value == e.value for e in cls):
+    ...             a = self.name
+    ...             e = cls(self.value).name
+    ...             raise ValueError(
+    ...                 "aliases not allowed in DuplicateFreeEnum:  %r --> %r"
+    ...                 % (a, e))
+    ...
+    >>> class Color(DuplicateFreeEnum):
+    ...     red = 1
+    ...     green = 2
+    ...     blue = 3
+    ...     grene = 2
+    Traceback (most recent call last):
+    ...
+    ValueError: aliases not allowed in DuplicateFreeEnum:  'grene' --> 'green'
+
+.. note::
+
+    This is a useful example for subclassing Enum to add or change other
+    behaviors as well as disallowing aliases.  If the only change desired is
+    no aliases allowed the :func:`unique` decorator can be used instead.
+
+
 Planet
 ------
 
index 775489bf950cd85e92a9d39e2c1c43ae68c723f1..38d95c5b4ccfa83f0e17dfde0f85b666a6f635ba 100644 (file)
@@ -4,7 +4,7 @@ import sys
 from collections import OrderedDict
 from types import MappingProxyType
 
-__all__ = ['Enum', 'IntEnum']
+__all__ = ['Enum', 'IntEnum', 'unique']
 
 
 class _RouteClassAttributeToGetattr:
@@ -463,3 +463,17 @@ class Enum(metaclass=EnumMeta):
 
 class IntEnum(int, Enum):
     """Enum where members are also (and must be) ints"""
+
+
+def unique(enumeration):
+    """Class decorator for enumerations ensuring unique member values."""
+    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
index 75b265686248584c570b5ed4ea4ed9de842f99e9..2b87c562da79cb39aece27b843b9906dabd7712f 100644 (file)
@@ -2,7 +2,7 @@ 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:
@@ -917,5 +917,38 @@ class TestEnum(unittest.TestCase):
         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()