bpo-29577: Enum: mixin classes don't mix well with already mixed Enums (GH-9328)
* bpo-29577: allow multiple mixin classes
This commit is contained in:
parent
fd97d1f1af
commit
5bdab641da
|
@ -387,10 +387,17 @@ whatever value(s) were given to the enum member will be passed into those
|
|||
methods. See `Planet`_ for an example.
|
||||
|
||||
|
||||
Restricted subclassing of enumerations
|
||||
--------------------------------------
|
||||
Restricted Enum subclassing
|
||||
---------------------------
|
||||
|
||||
Subclassing an enumeration is allowed only if the enumeration does not define
|
||||
A new :class:`Enum` class must have one base Enum class, up to one concrete
|
||||
data type, and as many :class:`object`-based mixin classes as needed. The
|
||||
order of these base classes is::
|
||||
|
||||
def EnumName([mix-in, ...,] [data-type,] base-enum):
|
||||
pass
|
||||
|
||||
Also, subclassing an enumeration is allowed only if the enumeration does not define
|
||||
any members. So this is forbidden::
|
||||
|
||||
>>> class MoreColor(Color):
|
||||
|
|
48
Lib/enum.py
48
Lib/enum.py
|
@ -480,37 +480,25 @@ class EnumMeta(type):
|
|||
if not bases:
|
||||
return object, Enum
|
||||
|
||||
# double check that we are not subclassing a class with existing
|
||||
# enumeration members; while we're at it, see if any other data
|
||||
# type has been mixed in so we can use the correct __new__
|
||||
member_type = first_enum = None
|
||||
for base in bases:
|
||||
if (base is not Enum and
|
||||
issubclass(base, Enum) and
|
||||
base._member_names_):
|
||||
raise TypeError("Cannot extend enumerations")
|
||||
# base is now the last base in bases
|
||||
if not issubclass(base, Enum):
|
||||
raise TypeError("new enumerations must be created as "
|
||||
"`ClassName([mixin_type,] enum_type)`")
|
||||
def _find_data_type(bases):
|
||||
for chain in bases:
|
||||
for base in chain.__mro__:
|
||||
if base is object:
|
||||
continue
|
||||
elif '__new__' in base.__dict__:
|
||||
if issubclass(base, Enum) and not hasattr(base, '__new_member__'):
|
||||
continue
|
||||
return base
|
||||
|
||||
# get correct mix-in type (either mix-in type of Enum subclass, or
|
||||
# first base if last base is Enum)
|
||||
if not issubclass(bases[0], Enum):
|
||||
member_type = bases[0] # first data type
|
||||
first_enum = bases[-1] # enum type
|
||||
else:
|
||||
for base in bases[0].__mro__:
|
||||
# most common: (IntEnum, int, Enum, object)
|
||||
# possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
|
||||
# <class 'int'>, <Enum 'Enum'>,
|
||||
# <class 'object'>)
|
||||
if issubclass(base, Enum):
|
||||
if first_enum is None:
|
||||
first_enum = base
|
||||
else:
|
||||
if member_type is None:
|
||||
member_type = base
|
||||
# ensure final parent class is an Enum derivative, find any concrete
|
||||
# data type, and check that Enum has no members
|
||||
first_enum = bases[-1]
|
||||
if not issubclass(first_enum, Enum):
|
||||
raise TypeError("new enumerations should be created as "
|
||||
"`EnumName([mixin_type, ...] [data_type,] enum_type)`")
|
||||
member_type = _find_data_type(bases) or object
|
||||
if first_enum._member_names_:
|
||||
raise TypeError("Cannot extend enumerations")
|
||||
|
||||
return member_type, first_enum
|
||||
|
||||
|
|
|
@ -122,6 +122,22 @@ class TestHelpers(unittest.TestCase):
|
|||
'__', '___', '____', '_____',):
|
||||
self.assertFalse(enum._is_dunder(s))
|
||||
|
||||
# for subclassing tests
|
||||
|
||||
class classproperty:
|
||||
|
||||
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
|
||||
self.fget = fget
|
||||
self.fset = fset
|
||||
self.fdel = fdel
|
||||
if doc is None and fget is not None:
|
||||
doc = fget.__doc__
|
||||
self.__doc__ = doc
|
||||
|
||||
def __get__(self, instance, ownerclass):
|
||||
return self.fget(ownerclass)
|
||||
|
||||
|
||||
# tests
|
||||
|
||||
class TestEnum(unittest.TestCase):
|
||||
|
@ -1730,6 +1746,102 @@ class TestEnum(unittest.TestCase):
|
|||
else:
|
||||
raise Exception('Exception not raised.')
|
||||
|
||||
def test_multiple_mixin(self):
|
||||
class MaxMixin:
|
||||
@classproperty
|
||||
def MAX(cls):
|
||||
max = len(cls)
|
||||
cls.MAX = max
|
||||
return max
|
||||
class StrMixin:
|
||||
def __str__(self):
|
||||
return self._name_.lower()
|
||||
class SomeEnum(Enum):
|
||||
def behavior(self):
|
||||
return 'booyah'
|
||||
class AnotherEnum(Enum):
|
||||
def behavior(self):
|
||||
return 'nuhuh!'
|
||||
def social(self):
|
||||
return "what's up?"
|
||||
class Color(MaxMixin, Enum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 3)
|
||||
self.assertEqual(Color.MAX, 3)
|
||||
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
|
||||
class Color(MaxMixin, StrMixin, Enum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 3)
|
||||
self.assertEqual(Color.MAX, 3)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
class Color(StrMixin, MaxMixin, Enum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 3)
|
||||
self.assertEqual(Color.MAX, 3)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
class CoolColor(StrMixin, SomeEnum, Enum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(CoolColor.RED.value, 1)
|
||||
self.assertEqual(CoolColor.GREEN.value, 2)
|
||||
self.assertEqual(CoolColor.BLUE.value, 3)
|
||||
self.assertEqual(str(CoolColor.BLUE), 'blue')
|
||||
self.assertEqual(CoolColor.RED.behavior(), 'booyah')
|
||||
class CoolerColor(StrMixin, AnotherEnum, Enum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(CoolerColor.RED.value, 1)
|
||||
self.assertEqual(CoolerColor.GREEN.value, 2)
|
||||
self.assertEqual(CoolerColor.BLUE.value, 3)
|
||||
self.assertEqual(str(CoolerColor.BLUE), 'blue')
|
||||
self.assertEqual(CoolerColor.RED.behavior(), 'nuhuh!')
|
||||
self.assertEqual(CoolerColor.RED.social(), "what's up?")
|
||||
class CoolestColor(StrMixin, SomeEnum, AnotherEnum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(CoolestColor.RED.value, 1)
|
||||
self.assertEqual(CoolestColor.GREEN.value, 2)
|
||||
self.assertEqual(CoolestColor.BLUE.value, 3)
|
||||
self.assertEqual(str(CoolestColor.BLUE), 'blue')
|
||||
self.assertEqual(CoolestColor.RED.behavior(), 'booyah')
|
||||
self.assertEqual(CoolestColor.RED.social(), "what's up?")
|
||||
class ConfusedColor(StrMixin, AnotherEnum, SomeEnum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(ConfusedColor.RED.value, 1)
|
||||
self.assertEqual(ConfusedColor.GREEN.value, 2)
|
||||
self.assertEqual(ConfusedColor.BLUE.value, 3)
|
||||
self.assertEqual(str(ConfusedColor.BLUE), 'blue')
|
||||
self.assertEqual(ConfusedColor.RED.behavior(), 'nuhuh!')
|
||||
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
|
||||
class ReformedColor(StrMixin, IntEnum, SomeEnum, AnotherEnum):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(ReformedColor.RED.value, 1)
|
||||
self.assertEqual(ReformedColor.GREEN.value, 2)
|
||||
self.assertEqual(ReformedColor.BLUE.value, 3)
|
||||
self.assertEqual(str(ReformedColor.BLUE), 'blue')
|
||||
self.assertEqual(ReformedColor.RED.behavior(), 'booyah')
|
||||
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
|
||||
self.assertTrue(issubclass(ReformedColor, int))
|
||||
|
||||
|
||||
class TestOrder(unittest.TestCase):
|
||||
|
||||
|
@ -2093,6 +2205,49 @@ class TestFlag(unittest.TestCase):
|
|||
d = 6
|
||||
self.assertEqual(repr(Bizarre(7)), '<Bizarre.d|c|b: 7>')
|
||||
|
||||
def test_multiple_mixin(self):
|
||||
class AllMixin:
|
||||
@classproperty
|
||||
def ALL(cls):
|
||||
members = list(cls)
|
||||
all_value = None
|
||||
if members:
|
||||
all_value = members[0]
|
||||
for member in members[1:]:
|
||||
all_value |= member
|
||||
cls.ALL = all_value
|
||||
return all_value
|
||||
class StrMixin:
|
||||
def __str__(self):
|
||||
return self._name_.lower()
|
||||
class Color(AllMixin, Flag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
|
||||
class Color(AllMixin, StrMixin, Flag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
class Color(StrMixin, AllMixin, Flag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
|
||||
@support.reap_threads
|
||||
def test_unique_composite(self):
|
||||
# override __eq__ to be identity only
|
||||
|
@ -2468,6 +2623,49 @@ class TestIntFlag(unittest.TestCase):
|
|||
for f in Open:
|
||||
self.assertEqual(bool(f.value), bool(f))
|
||||
|
||||
def test_multiple_mixin(self):
|
||||
class AllMixin:
|
||||
@classproperty
|
||||
def ALL(cls):
|
||||
members = list(cls)
|
||||
all_value = None
|
||||
if members:
|
||||
all_value = members[0]
|
||||
for member in members[1:]:
|
||||
all_value |= member
|
||||
cls.ALL = all_value
|
||||
return all_value
|
||||
class StrMixin:
|
||||
def __str__(self):
|
||||
return self._name_.lower()
|
||||
class Color(AllMixin, IntFlag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
|
||||
class Color(AllMixin, StrMixin, IntFlag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
class Color(StrMixin, AllMixin, IntFlag):
|
||||
RED = auto()
|
||||
GREEN = auto()
|
||||
BLUE = auto()
|
||||
self.assertEqual(Color.RED.value, 1)
|
||||
self.assertEqual(Color.GREEN.value, 2)
|
||||
self.assertEqual(Color.BLUE.value, 4)
|
||||
self.assertEqual(Color.ALL.value, 7)
|
||||
self.assertEqual(str(Color.BLUE), 'blue')
|
||||
|
||||
@support.reap_threads
|
||||
def test_unique_composite(self):
|
||||
# override __eq__ to be identity only
|
||||
|
@ -2553,6 +2751,7 @@ class TestUnique(unittest.TestCase):
|
|||
value = 4
|
||||
|
||||
|
||||
|
||||
expected_help_output_with_docs = """\
|
||||
Help on class Color in module %s:
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Support multiple mixin classes when creating Enums.
|
Loading…
Reference in New Issue