From f24bb35a69d18a05047399eadc63b4be092aee71 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 18 Jul 2013 17:05:39 -0700 Subject: closes issue18042 -- a `unique` decorator is added to enum.py The docs also clarify the 'Interesting Example' duplicate-free enum is for demonstration purposes. --- Doc/library/enum.rst | 93 ++++++++++++++++++++++++++++++++++++--------------- Lib/enum.py | 16 ++++++++- Lib/test/test_enum.py | 35 ++++++++++++++++++- 3 files changed, 115 insertions(+), 29 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index b919bdc..1e464d7 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 775489b..38d95c5 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 75b2656..2b87c56 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() -- cgit v0.12