diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index b919bdc8397..1e464d7361d 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -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) + +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 : 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 ------ diff --git a/Lib/enum.py b/Lib/enum.py index 775489bf950..38d95c5b4cc 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -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 diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 75b26568624..2b87c562da7 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -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()