closes issue18042 -- a `unique` decorator is added to enum.py

The docs also clarify the 'Interesting Example' duplicate-free enum is for
demonstration purposes.
This commit is contained in:
Ethan Furman 2013-07-18 17:05:39 -07:00
parent d85032e25d
commit f24bb35a69
3 changed files with 115 additions and 29 deletions

View 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
------

View 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

View 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()