Skip to content

Commit 5bdab64

Browse files
authored
bpo-29577: Enum: mixin classes don't mix well with already mixed Enums (GH-9328)
* bpo-29577: allow multiple mixin classes
1 parent fd97d1f commit 5bdab64

File tree

4 files changed

+229
-34
lines changed

4 files changed

+229
-34
lines changed

Doc/library/enum.rst

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,10 +387,17 @@ whatever value(s) were given to the enum member will be passed into those
387387
methods. See `Planet`_ for an example.
388388

389389

390-
Restricted subclassing of enumerations
391-
--------------------------------------
390+
Restricted Enum subclassing
391+
---------------------------
392392

393-
Subclassing an enumeration is allowed only if the enumeration does not define
393+
A new :class:`Enum` class must have one base Enum class, up to one concrete
394+
data type, and as many :class:`object`-based mixin classes as needed. The
395+
order of these base classes is::
396+
397+
def EnumName([mix-in, ...,] [data-type,] base-enum):
398+
pass
399+
400+
Also, subclassing an enumeration is allowed only if the enumeration does not define
394401
any members. So this is forbidden::
395402

396403
>>> class MoreColor(Color):

Lib/enum.py

Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -480,37 +480,25 @@ def _get_mixins_(bases):
480480
if not bases:
481481
return object, Enum
482482

483-
# double check that we are not subclassing a class with existing
484-
# enumeration members; while we're at it, see if any other data
485-
# type has been mixed in so we can use the correct __new__
486-
member_type = first_enum = None
487-
for base in bases:
488-
if (base is not Enum and
489-
issubclass(base, Enum) and
490-
base._member_names_):
491-
raise TypeError("Cannot extend enumerations")
492-
# base is now the last base in bases
493-
if not issubclass(base, Enum):
494-
raise TypeError("new enumerations must be created as "
495-
"`ClassName([mixin_type,] enum_type)`")
496-
497-
# get correct mix-in type (either mix-in type of Enum subclass, or
498-
# first base if last base is Enum)
499-
if not issubclass(bases[0], Enum):
500-
member_type = bases[0] # first data type
501-
first_enum = bases[-1] # enum type
502-
else:
503-
for base in bases[0].__mro__:
504-
# most common: (IntEnum, int, Enum, object)
505-
# possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
506-
# <class 'int'>, <Enum 'Enum'>,
507-
# <class 'object'>)
508-
if issubclass(base, Enum):
509-
if first_enum is None:
510-
first_enum = base
511-
else:
512-
if member_type is None:
513-
member_type = base
483+
def _find_data_type(bases):
484+
for chain in bases:
485+
for base in chain.__mro__:
486+
if base is object:
487+
continue
488+
elif '__new__' in base.__dict__:
489+
if issubclass(base, Enum) and not hasattr(base, '__new_member__'):
490+
continue
491+
return base
492+
493+
# ensure final parent class is an Enum derivative, find any concrete
494+
# data type, and check that Enum has no members
495+
first_enum = bases[-1]
496+
if not issubclass(first_enum, Enum):
497+
raise TypeError("new enumerations should be created as "
498+
"`EnumName([mixin_type, ...] [data_type,] enum_type)`")
499+
member_type = _find_data_type(bases) or object
500+
if first_enum._member_names_:
501+
raise TypeError("Cannot extend enumerations")
514502

515503
return member_type, first_enum
516504

Lib/test/test_enum.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@ def test_is_dunder(self):
122122
'__', '___', '____', '_____',):
123123
self.assertFalse(enum._is_dunder(s))
124124

125+
# for subclassing tests
126+
127+
class classproperty:
128+
129+
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
130+
self.fget = fget
131+
self.fset = fset
132+
self.fdel = fdel
133+
if doc is None and fget is not None:
134+
doc = fget.__doc__
135+
self.__doc__ = doc
136+
137+
def __get__(self, instance, ownerclass):
138+
return self.fget(ownerclass)
139+
140+
125141
# tests
126142

127143
class TestEnum(unittest.TestCase):
@@ -1730,6 +1746,102 @@ def _missing_(cls, item):
17301746
else:
17311747
raise Exception('Exception not raised.')
17321748

1749+
def test_multiple_mixin(self):
1750+
class MaxMixin:
1751+
@classproperty
1752+
def MAX(cls):
1753+
max = len(cls)
1754+
cls.MAX = max
1755+
return max
1756+
class StrMixin:
1757+
def __str__(self):
1758+
return self._name_.lower()
1759+
class SomeEnum(Enum):
1760+
def behavior(self):
1761+
return 'booyah'
1762+
class AnotherEnum(Enum):
1763+
def behavior(self):
1764+
return 'nuhuh!'
1765+
def social(self):
1766+
return "what's up?"
1767+
class Color(MaxMixin, Enum):
1768+
RED = auto()
1769+
GREEN = auto()
1770+
BLUE = auto()
1771+
self.assertEqual(Color.RED.value, 1)
1772+
self.assertEqual(Color.GREEN.value, 2)
1773+
self.assertEqual(Color.BLUE.value, 3)
1774+
self.assertEqual(Color.MAX, 3)
1775+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
1776+
class Color(MaxMixin, StrMixin, Enum):
1777+
RED = auto()
1778+
GREEN = auto()
1779+
BLUE = auto()
1780+
self.assertEqual(Color.RED.value, 1)
1781+
self.assertEqual(Color.GREEN.value, 2)
1782+
self.assertEqual(Color.BLUE.value, 3)
1783+
self.assertEqual(Color.MAX, 3)
1784+
self.assertEqual(str(Color.BLUE), 'blue')
1785+
class Color(StrMixin, MaxMixin, Enum):
1786+
RED = auto()
1787+
GREEN = auto()
1788+
BLUE = auto()
1789+
self.assertEqual(Color.RED.value, 1)
1790+
self.assertEqual(Color.GREEN.value, 2)
1791+
self.assertEqual(Color.BLUE.value, 3)
1792+
self.assertEqual(Color.MAX, 3)
1793+
self.assertEqual(str(Color.BLUE), 'blue')
1794+
class CoolColor(StrMixin, SomeEnum, Enum):
1795+
RED = auto()
1796+
GREEN = auto()
1797+
BLUE = auto()
1798+
self.assertEqual(CoolColor.RED.value, 1)
1799+
self.assertEqual(CoolColor.GREEN.value, 2)
1800+
self.assertEqual(CoolColor.BLUE.value, 3)
1801+
self.assertEqual(str(CoolColor.BLUE), 'blue')
1802+
self.assertEqual(CoolColor.RED.behavior(), 'booyah')
1803+
class CoolerColor(StrMixin, AnotherEnum, Enum):
1804+
RED = auto()
1805+
GREEN = auto()
1806+
BLUE = auto()
1807+
self.assertEqual(CoolerColor.RED.value, 1)
1808+
self.assertEqual(CoolerColor.GREEN.value, 2)
1809+
self.assertEqual(CoolerColor.BLUE.value, 3)
1810+
self.assertEqual(str(CoolerColor.BLUE), 'blue')
1811+
self.assertEqual(CoolerColor.RED.behavior(), 'nuhuh!')
1812+
self.assertEqual(CoolerColor.RED.social(), "what's up?")
1813+
class CoolestColor(StrMixin, SomeEnum, AnotherEnum):
1814+
RED = auto()
1815+
GREEN = auto()
1816+
BLUE = auto()
1817+
self.assertEqual(CoolestColor.RED.value, 1)
1818+
self.assertEqual(CoolestColor.GREEN.value, 2)
1819+
self.assertEqual(CoolestColor.BLUE.value, 3)
1820+
self.assertEqual(str(CoolestColor.BLUE), 'blue')
1821+
self.assertEqual(CoolestColor.RED.behavior(), 'booyah')
1822+
self.assertEqual(CoolestColor.RED.social(), "what's up?")
1823+
class ConfusedColor(StrMixin, AnotherEnum, SomeEnum):
1824+
RED = auto()
1825+
GREEN = auto()
1826+
BLUE = auto()
1827+
self.assertEqual(ConfusedColor.RED.value, 1)
1828+
self.assertEqual(ConfusedColor.GREEN.value, 2)
1829+
self.assertEqual(ConfusedColor.BLUE.value, 3)
1830+
self.assertEqual(str(ConfusedColor.BLUE), 'blue')
1831+
self.assertEqual(ConfusedColor.RED.behavior(), 'nuhuh!')
1832+
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
1833+
class ReformedColor(StrMixin, IntEnum, SomeEnum, AnotherEnum):
1834+
RED = auto()
1835+
GREEN = auto()
1836+
BLUE = auto()
1837+
self.assertEqual(ReformedColor.RED.value, 1)
1838+
self.assertEqual(ReformedColor.GREEN.value, 2)
1839+
self.assertEqual(ReformedColor.BLUE.value, 3)
1840+
self.assertEqual(str(ReformedColor.BLUE), 'blue')
1841+
self.assertEqual(ReformedColor.RED.behavior(), 'booyah')
1842+
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
1843+
self.assertTrue(issubclass(ReformedColor, int))
1844+
17331845

17341846
class TestOrder(unittest.TestCase):
17351847

@@ -2093,6 +2205,49 @@ class Bizarre(Flag):
20932205
d = 6
20942206
self.assertEqual(repr(Bizarre(7)), '<Bizarre.d|c|b: 7>')
20952207

2208+
def test_multiple_mixin(self):
2209+
class AllMixin:
2210+
@classproperty
2211+
def ALL(cls):
2212+
members = list(cls)
2213+
all_value = None
2214+
if members:
2215+
all_value = members[0]
2216+
for member in members[1:]:
2217+
all_value |= member
2218+
cls.ALL = all_value
2219+
return all_value
2220+
class StrMixin:
2221+
def __str__(self):
2222+
return self._name_.lower()
2223+
class Color(AllMixin, Flag):
2224+
RED = auto()
2225+
GREEN = auto()
2226+
BLUE = auto()
2227+
self.assertEqual(Color.RED.value, 1)
2228+
self.assertEqual(Color.GREEN.value, 2)
2229+
self.assertEqual(Color.BLUE.value, 4)
2230+
self.assertEqual(Color.ALL.value, 7)
2231+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
2232+
class Color(AllMixin, StrMixin, Flag):
2233+
RED = auto()
2234+
GREEN = auto()
2235+
BLUE = auto()
2236+
self.assertEqual(Color.RED.value, 1)
2237+
self.assertEqual(Color.GREEN.value, 2)
2238+
self.assertEqual(Color.BLUE.value, 4)
2239+
self.assertEqual(Color.ALL.value, 7)
2240+
self.assertEqual(str(Color.BLUE), 'blue')
2241+
class Color(StrMixin, AllMixin, Flag):
2242+
RED = auto()
2243+
GREEN = auto()
2244+
BLUE = auto()
2245+
self.assertEqual(Color.RED.value, 1)
2246+
self.assertEqual(Color.GREEN.value, 2)
2247+
self.assertEqual(Color.BLUE.value, 4)
2248+
self.assertEqual(Color.ALL.value, 7)
2249+
self.assertEqual(str(Color.BLUE), 'blue')
2250+
20962251
@support.reap_threads
20972252
def test_unique_composite(self):
20982253
# override __eq__ to be identity only
@@ -2468,6 +2623,49 @@ def test_bool(self):
24682623
for f in Open:
24692624
self.assertEqual(bool(f.value), bool(f))
24702625

2626+
def test_multiple_mixin(self):
2627+
class AllMixin:
2628+
@classproperty
2629+
def ALL(cls):
2630+
members = list(cls)
2631+
all_value = None
2632+
if members:
2633+
all_value = members[0]
2634+
for member in members[1:]:
2635+
all_value |= member
2636+
cls.ALL = all_value
2637+
return all_value
2638+
class StrMixin:
2639+
def __str__(self):
2640+
return self._name_.lower()
2641+
class Color(AllMixin, IntFlag):
2642+
RED = auto()
2643+
GREEN = auto()
2644+
BLUE = auto()
2645+
self.assertEqual(Color.RED.value, 1)
2646+
self.assertEqual(Color.GREEN.value, 2)
2647+
self.assertEqual(Color.BLUE.value, 4)
2648+
self.assertEqual(Color.ALL.value, 7)
2649+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
2650+
class Color(AllMixin, StrMixin, IntFlag):
2651+
RED = auto()
2652+
GREEN = auto()
2653+
BLUE = auto()
2654+
self.assertEqual(Color.RED.value, 1)
2655+
self.assertEqual(Color.GREEN.value, 2)
2656+
self.assertEqual(Color.BLUE.value, 4)
2657+
self.assertEqual(Color.ALL.value, 7)
2658+
self.assertEqual(str(Color.BLUE), 'blue')
2659+
class Color(StrMixin, AllMixin, IntFlag):
2660+
RED = auto()
2661+
GREEN = auto()
2662+
BLUE = auto()
2663+
self.assertEqual(Color.RED.value, 1)
2664+
self.assertEqual(Color.GREEN.value, 2)
2665+
self.assertEqual(Color.BLUE.value, 4)
2666+
self.assertEqual(Color.ALL.value, 7)
2667+
self.assertEqual(str(Color.BLUE), 'blue')
2668+
24712669
@support.reap_threads
24722670
def test_unique_composite(self):
24732671
# override __eq__ to be identity only
@@ -2553,6 +2751,7 @@ class Sillier(IntEnum):
25532751
value = 4
25542752

25552753

2754+
25562755
expected_help_output_with_docs = """\
25572756
Help on class Color in module %s:
25582757
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support multiple mixin classes when creating Enums.

0 commit comments

Comments
 (0)