From 65a5a47d799184bf69c8f0eb6004c701ff8f7d90 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 1 Sep 2016 23:55:19 -0700 Subject: issue23591: add docs; code cleanup; more tests --- Doc/library/enum.rst | 173 ++++++++++++++++++++++++++++++++++++-------------- Lib/enum.py | 8 +-- Lib/test/test_enum.py | 88 ++++++++++++++++++++----- 3 files changed, 202 insertions(+), 67 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 827bab0..72218b9 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -23,9 +23,9 @@ by identity, and the enumeration itself can be iterated over. Module Contents --------------- -This module defines two enumeration classes that can be used to define unique -sets of names and values: :class:`Enum` and :class:`IntEnum`. It also defines -one decorator, :func:`unique`. +This module defines four enumeration classes that can be used to define unique +sets of names and values: :class:`Enum`, :class:`IntEnum`, and +:class:`IntFlags`. It also defines one decorator, :func:`unique`. .. class:: Enum @@ -37,10 +37,23 @@ one decorator, :func:`unique`. Base class for creating enumerated constants that are also subclasses of :class:`int`. +.. class:: IntFlag + + Base class for creating enumerated constants that can be combined using + the bitwise operators without losing their :class:`IntFlag` membership. + :class:`IntFlag` members are also subclasses of :class:`int`. + +.. class:: Flag + + Base class for creating enumerated constants that can be combined using + the bitwise operations without losing their :class:`Flag` membership. + .. function:: unique Enum class decorator that ensures only one name is bound to any one value. +.. versionadded:: 3.6 ``Flag``, ``IntFlag`` + Creating an Enum ---------------- @@ -478,7 +491,7 @@ Derived Enumerations IntEnum ^^^^^^^ -A variation of :class:`Enum` is provided which is also a subclass of +The first variation of :class:`Enum` that is provided is also a subclass of :class:`int`. Members of an :class:`IntEnum` can be compared to integers; by extension, integer enumerations of different types can also be compared to each other:: @@ -521,13 +534,54 @@ However, they still can't be compared to standard :class:`Enum` enumerations:: >>> [i for i in range(Shape.square)] [0, 1] -For the vast majority of code, :class:`Enum` is strongly recommended, -since :class:`IntEnum` breaks some semantic promises of an enumeration (by -being comparable to integers, and thus by transitivity to other -unrelated enumerations). It should be used only in special cases where -there's no other choice; for example, when integer constants are -replaced with enumerations and backwards compatibility is required with code -that still expects integers. + +IntFlag +^^^^^^^ + +The next variation of :class:`Enum` provided, :class:`IntFlag`, is also based +on :class:`int`. The difference being :class:`IntFlag` members can be combined +using the bitwise operators (&, \|, ^, ~) and the result is still an +:class:`IntFlag` member. However, as the name implies, :class:`IntFlag` +members also subclass :class:`int` and can be used wherever an :class:`int` is. +Any operation on an :class:`IntFlag` member besides the bit-wise operations +will lose the :class:`IntFlag` membership. + + >>> from enum import IntFlag + >>> class Perm(IntFlag): + ... R = 4 + ... W = 2 + ... X = 1 + ... + >>> Perm.R | Perm.W + + >>> Perm.R + Perm.W + 6 + >>> RW = Perm.R | Perm.W + >>> Perm.R in RW + True + +.. versionadded:: 3.6 + + +Flag +^^^^ + +The last variation is :class:`Flag`. Like :class:`IntFlag`, :class:`Flag` +members can be combined using the bitwise operators (^, \|, ^, ~). Unlike +:class:`IntFlag`, they cannot be combined with, nor compared against, any +other :class:`Flag` enumeration nor :class:`int`. + +.. versionadded:: 3.6 + +.. note:: + + For the majority of new code, :class:`Enum` and :class:`Flag` are strongly + recommended, since :class:`IntEnum` and :class:`IntFlag` break some + semantic promises of an enumeration (by being comparable to integers, and + thus by transitivity to other unrelated enumerations). :class:`IntEnum` + and :class:`IntFlag` should be used only in cases where :class:`Enum` and + :class:`Flag` will not do; for example, when integer constants are replaced + with enumerations, or for interoperability with other systems. Others @@ -567,10 +621,10 @@ Some rules: Interesting examples -------------------- -While :class:`Enum` and :class:`IntEnum` are expected to cover the majority of -use-cases, they cannot cover them all. Here are recipes for some different -types of enumerations that can be used directly, or as examples for creating -one's own. +While :class:`Enum`, :class:`IntEnum`, :class:`IntFlag`, and :class:`Flag` are +expected to cover the majority of use-cases, they cannot cover them all. Here +are recipes for some different types of enumerations that can be used directly, +or as examples for creating one's own. AutoNumber @@ -731,10 +785,56 @@ member instances. Finer Points ^^^^^^^^^^^^ +Supported ``__dunder__`` names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:attr:`__members__` is an :class:`OrderedDict` of ``member_name``:``member`` +items. It is only available on the class. + +:meth:`__new__`, if specified, must create and return the enum members; it is +also a very good idea to set the member's :attr:`_value_` appropriately. Once +all the members are created it is no longer used. + + +Supported ``_sunder_`` names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``_name_`` -- name of the member +- ``_value_`` -- value of the member; can be set / modified in ``__new__`` + +- ``_missing_`` -- a lookup function used when a value is not found; may be + overridden +- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent + (class attribute, removed during class creation) + +.. versionadded:: 3.6 ``_missing_``, ``_order_`` + +To help keep Python 2 / Python 3 code in sync an :attr:`_order_` attribute can +be provided. It will be checked against the actual order of the enumeration +and raise an error if the two do not match:: + + >>> class Color(Enum): + ... _order_ = 'red green blue' + ... red = 1 + ... blue = 3 + ... green = 2 + ... + Traceback (most recent call last): + ... + TypeError: member order does not match _order_ + +.. note:: + + In Python 2 code the :attr:`_order_` attribute is necessary as definition + order is lost before it can be recorded. + +``Enum`` member type +~~~~~~~~~~~~~~~~~~~~ + :class:`Enum` members are instances of an :class:`Enum` class, and even though they are accessible as `EnumClass.member`, they should not be accessed directly from the member as that lookup may fail or, worse, return something -besides the :class:`Enum` member you looking for:: +besides the ``Enum`` member you looking for:: >>> class FieldTypes(Enum): ... name = 0 @@ -748,16 +848,24 @@ besides the :class:`Enum` member you looking for:: .. versionchanged:: 3.5 -Boolean evaluation: Enum classes that are mixed with non-Enum types (such as + +Boolean value of ``Enum`` classes and members +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Enum`` members that are mixed with non-Enum types (such as :class:`int`, :class:`str`, etc.) are evaluated according to the mixed-in -type's rules; otherwise, all members evaluate as ``True``. To make your own +type's rules; otherwise, all members evaluate as :data:`True`. To make your own Enum's boolean evaluation depend on the member's value add the following to your class:: def __bool__(self): return bool(self.value) -The :attr:`__members__` attribute is only available on the class. +``Enum`` classes always evaluate as :data:`True`. + + +``Enum`` classes with methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you give your :class:`Enum` subclass extra methods, like the `Planet`_ class above, those methods will show up in a :func:`dir` of the member, @@ -768,30 +876,3 @@ but not of the class:: >>> dir(Planet.EARTH) ['__class__', '__doc__', '__module__', 'name', 'surface_gravity', 'value'] -The :meth:`__new__` method will only be used for the creation of the -:class:`Enum` members -- after that it is replaced. Any custom :meth:`__new__` -method must create the object and set the :attr:`_value_` attribute -appropriately. - -If you wish to change how :class:`Enum` members are looked up you should either -write a helper function or a :func:`classmethod` for the :class:`Enum` -subclass. - -To help keep Python 2 / Python 3 code in sync a user-specified :attr:`_order_`, -if provided, will be checked to ensure the actual order of the enumeration -matches:: - - >>> class Color(Enum): - ... _order_ = 'red green blue' - ... red = 1 - ... blue = 3 - ... green = 2 - ... - Traceback (most recent call last): - ... - TypeError: member order does not match _order_ - -.. note:: - - In Python 2 code the :attr:`_order_` attribute is necessary as definition - order is lost during class creation. diff --git a/Lib/enum.py b/Lib/enum.py index e89c17d..8369631 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -10,7 +10,7 @@ except ImportError: from collections import OrderedDict -__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flags', 'IntFlags', 'unique'] +__all__ = ['EnumMeta', 'Enum', 'IntEnum', 'Flag', 'IntFlag', 'unique'] def _is_descriptor(obj): @@ -104,7 +104,7 @@ class EnumMeta(type): enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None) return enum_dict - def __new__(metacls, cls, bases, classdict, **kwds): + def __new__(metacls, cls, bases, classdict): # an Enum class is final once enumeration items have been defined; it # cannot be mixed with other types (int, float, etc.) if it has an # inherited __new__ unless a new __new__ is defined (or the resulting @@ -614,7 +614,7 @@ class IntEnum(int, Enum): def _reduce_ex_by_name(self, proto): return self.name -class Flags(Enum): +class Flag(Enum): """Support for flags""" @staticmethod def _generate_next_value_(name, start, count, last_value): @@ -736,7 +736,7 @@ class Flags(Enum): return self.__class__(inverted) -class IntFlags(int, Flags): +class IntFlag(int, Flag): """Support for integer-based Flags""" @classmethod diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 1831895..cfe1b18 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -3,7 +3,7 @@ import inspect import pydoc import unittest from collections import OrderedDict -from enum import Enum, IntEnum, EnumMeta, Flags, IntFlags, unique +from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -33,6 +33,14 @@ try: except Exception as exc: FloatStooges = exc +try: + class FlagStooges(Flag): + LARRY = 1 + CURLY = 2 + MOE = 3 +except Exception as exc: + FlagStooges = exc + # for pickle test and subclass tests try: class StrEnum(str, Enum): @@ -1633,13 +1641,13 @@ class TestOrder(unittest.TestCase): verde = green -class TestFlags(unittest.TestCase): +class TestFlag(unittest.TestCase): """Tests of the Flags.""" - class Perm(Flags): + class Perm(Flag): R, W, X = 4, 2, 1 - class Open(Flags): + class Open(Flag): RO = 0 WO = 1 RW = 2 @@ -1760,7 +1768,7 @@ class TestFlags(unittest.TestCase): self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) def test_programatic_function_string(self): - Perm = Flags('Perm', 'R W X') + Perm = Flag('Perm', 'R W X') lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -1775,7 +1783,7 @@ class TestFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_string_with_start(self): - Perm = Flags('Perm', 'R W X', start=8) + Perm = Flag('Perm', 'R W X', start=8) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -1790,7 +1798,7 @@ class TestFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_string_list(self): - Perm = Flags('Perm', ['R', 'W', 'X']) + Perm = Flag('Perm', ['R', 'W', 'X']) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -1805,7 +1813,7 @@ class TestFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_iterable(self): - Perm = Flags('Perm', (('R', 2), ('W', 8), ('X', 32))) + Perm = Flag('Perm', (('R', 2), ('W', 8), ('X', 32))) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -1820,7 +1828,7 @@ class TestFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_from_dict(self): - Perm = Flags('Perm', OrderedDict((('R', 2), ('W', 8), ('X', 32)))) + Perm = Flag('Perm', OrderedDict((('R', 2), ('W', 8), ('X', 32)))) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -1834,16 +1842,42 @@ class TestFlags(unittest.TestCase): self.assertIn(e, Perm) self.assertIs(type(e), Perm) + def test_pickle(self): + if isinstance(FlagStooges, Exception): + raise FlagStooges + test_pickle_dump_load(self.assertIs, FlagStooges.CURLY|FlagStooges.MOE) + test_pickle_dump_load(self.assertIs, FlagStooges) + -class TestIntFlags(unittest.TestCase): + def test_containment(self): + Perm = self.Perm + R, W, X = Perm + RW = R | W + RX = R | X + WX = W | X + RWX = R | W | X + self.assertTrue(R in RW) + self.assertTrue(R in RX) + self.assertTrue(R in RWX) + self.assertTrue(W in RW) + self.assertTrue(W in WX) + self.assertTrue(W in RWX) + self.assertTrue(X in RX) + self.assertTrue(X in WX) + self.assertTrue(X in RWX) + self.assertFalse(R in WX) + self.assertFalse(W in RX) + self.assertFalse(X in RW) + +class TestIntFlag(unittest.TestCase): """Tests of the IntFlags.""" - class Perm(IntFlags): + class Perm(IntFlag): X = 1 << 0 W = 1 << 1 R = 1 << 2 - class Open(IntFlags): + class Open(IntFlag): RO = 0 WO = 1 RW = 2 @@ -2003,7 +2037,7 @@ class TestIntFlags(unittest.TestCase): self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) def test_programatic_function_string(self): - Perm = IntFlags('Perm', 'R W X') + Perm = IntFlag('Perm', 'R W X') lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -2019,7 +2053,7 @@ class TestIntFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_string_with_start(self): - Perm = IntFlags('Perm', 'R W X', start=8) + Perm = IntFlag('Perm', 'R W X', start=8) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -2035,7 +2069,7 @@ class TestIntFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_string_list(self): - Perm = IntFlags('Perm', ['R', 'W', 'X']) + Perm = IntFlag('Perm', ['R', 'W', 'X']) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -2051,7 +2085,7 @@ class TestIntFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_iterable(self): - Perm = IntFlags('Perm', (('R', 2), ('W', 8), ('X', 32))) + Perm = IntFlag('Perm', (('R', 2), ('W', 8), ('X', 32))) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -2067,7 +2101,7 @@ class TestIntFlags(unittest.TestCase): self.assertIs(type(e), Perm) def test_programatic_function_from_dict(self): - Perm = IntFlags('Perm', OrderedDict((('R', 2), ('W', 8), ('X', 32)))) + Perm = IntFlag('Perm', OrderedDict((('R', 2), ('W', 8), ('X', 32)))) lst = list(Perm) self.assertEqual(len(lst), len(Perm)) self.assertEqual(len(Perm), 3, Perm) @@ -2083,6 +2117,26 @@ class TestIntFlags(unittest.TestCase): self.assertIs(type(e), Perm) + def test_containment(self): + Perm = self.Perm + R, W, X = Perm + RW = R | W + RX = R | X + WX = W | X + RWX = R | W | X + self.assertTrue(R in RW) + self.assertTrue(R in RX) + self.assertTrue(R in RWX) + self.assertTrue(W in RW) + self.assertTrue(W in WX) + self.assertTrue(W in RWX) + self.assertTrue(X in RX) + self.assertTrue(X in WX) + self.assertTrue(X in RWX) + self.assertFalse(R in WX) + self.assertFalse(W in RX) + self.assertFalse(X in RW) + class TestUnique(unittest.TestCase): def test_unique_clean(self): -- cgit v0.12