Skip to content

Commit 0c076ca

Browse files
authored
[3.7] bpo-29577: Enum: mixin classes don't mix well with already mixed Enums (pythonGH-9328) (pythonGH-9486)
* bpo-29577: allow multiple mixin classes
1 parent c00f703 commit 0c076ca

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
@@ -449,37 +449,25 @@ def _get_mixins_(bases):
449449
if not bases:
450450
return object, Enum
451451

452-
# double check that we are not subclassing a class with existing
453-
# enumeration members; while we're at it, see if any other data
454-
# type has been mixed in so we can use the correct __new__
455-
member_type = first_enum = None
456-
for base in bases:
457-
if (base is not Enum and
458-
issubclass(base, Enum) and
459-
base._member_names_):
460-
raise TypeError("Cannot extend enumerations")
461-
# base is now the last base in bases
462-
if not issubclass(base, Enum):
463-
raise TypeError("new enumerations must be created as "
464-
"`ClassName([mixin_type,] enum_type)`")
465-
466-
# get correct mix-in type (either mix-in type of Enum subclass, or
467-
# first base if last base is Enum)
468-
if not issubclass(bases[0], Enum):
469-
member_type = bases[0] # first data type
470-
first_enum = bases[-1] # enum type
471-
else:
472-
for base in bases[0].__mro__:
473-
# most common: (IntEnum, int, Enum, object)
474-
# possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
475-
# <class 'int'>, <Enum 'Enum'>,
476-
# <class 'object'>)
477-
if issubclass(base, Enum):
478-
if first_enum is None:
479-
first_enum = base
480-
else:
481-
if member_type is None:
482-
member_type = base
452+
def _find_data_type(bases):
453+
for chain in bases:
454+
for base in chain.__mro__:
455+
if base is object:
456+
continue
457+
elif '__new__' in base.__dict__:
458+
if issubclass(base, Enum) and not hasattr(base, '__new_member__'):
459+
continue
460+
return base
461+
462+
# ensure final parent class is an Enum derivative, find any concrete
463+
# data type, and check that Enum has no members
464+
first_enum = bases[-1]
465+
if not issubclass(first_enum, Enum):
466+
raise TypeError("new enumerations should be created as "
467+
"`EnumName([mixin_type, ...] [data_type,] enum_type)`")
468+
member_type = _find_data_type(bases) or object
469+
if first_enum._member_names_:
470+
raise TypeError("Cannot extend enumerations")
483471

484472
return member_type, first_enum
485473

Lib/test/test_enum.py

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

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

125141
class TestEnum(unittest.TestCase):
@@ -1701,6 +1717,102 @@ class Dupes(Enum):
17011717
third = auto()
17021718
self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes))
17031719

1720+
def test_multiple_mixin(self):
1721+
class MaxMixin:
1722+
@classproperty
1723+
def MAX(cls):
1724+
max = len(cls)
1725+
cls.MAX = max
1726+
return max
1727+
class StrMixin:
1728+
def __str__(self):
1729+
return self._name_.lower()
1730+
class SomeEnum(Enum):
1731+
def behavior(self):
1732+
return 'booyah'
1733+
class AnotherEnum(Enum):
1734+
def behavior(self):
1735+
return 'nuhuh!'
1736+
def social(self):
1737+
return "what's up?"
1738+
class Color(MaxMixin, Enum):
1739+
RED = auto()
1740+
GREEN = auto()
1741+
BLUE = auto()
1742+
self.assertEqual(Color.RED.value, 1)
1743+
self.assertEqual(Color.GREEN.value, 2)
1744+
self.assertEqual(Color.BLUE.value, 3)
1745+
self.assertEqual(Color.MAX, 3)
1746+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
1747+
class Color(MaxMixin, StrMixin, Enum):
1748+
RED = auto()
1749+
GREEN = auto()
1750+
BLUE = auto()
1751+
self.assertEqual(Color.RED.value, 1)
1752+
self.assertEqual(Color.GREEN.value, 2)
1753+
self.assertEqual(Color.BLUE.value, 3)
1754+
self.assertEqual(Color.MAX, 3)
1755+
self.assertEqual(str(Color.BLUE), 'blue')
1756+
class Color(StrMixin, MaxMixin, Enum):
1757+
RED = auto()
1758+
GREEN = auto()
1759+
BLUE = auto()
1760+
self.assertEqual(Color.RED.value, 1)
1761+
self.assertEqual(Color.GREEN.value, 2)
1762+
self.assertEqual(Color.BLUE.value, 3)
1763+
self.assertEqual(Color.MAX, 3)
1764+
self.assertEqual(str(Color.BLUE), 'blue')
1765+
class CoolColor(StrMixin, SomeEnum, Enum):
1766+
RED = auto()
1767+
GREEN = auto()
1768+
BLUE = auto()
1769+
self.assertEqual(CoolColor.RED.value, 1)
1770+
self.assertEqual(CoolColor.GREEN.value, 2)
1771+
self.assertEqual(CoolColor.BLUE.value, 3)
1772+
self.assertEqual(str(CoolColor.BLUE), 'blue')
1773+
self.assertEqual(CoolColor.RED.behavior(), 'booyah')
1774+
class CoolerColor(StrMixin, AnotherEnum, Enum):
1775+
RED = auto()
1776+
GREEN = auto()
1777+
BLUE = auto()
1778+
self.assertEqual(CoolerColor.RED.value, 1)
1779+
self.assertEqual(CoolerColor.GREEN.value, 2)
1780+
self.assertEqual(CoolerColor.BLUE.value, 3)
1781+
self.assertEqual(str(CoolerColor.BLUE), 'blue')
1782+
self.assertEqual(CoolerColor.RED.behavior(), 'nuhuh!')
1783+
self.assertEqual(CoolerColor.RED.social(), "what's up?")
1784+
class CoolestColor(StrMixin, SomeEnum, AnotherEnum):
1785+
RED = auto()
1786+
GREEN = auto()
1787+
BLUE = auto()
1788+
self.assertEqual(CoolestColor.RED.value, 1)
1789+
self.assertEqual(CoolestColor.GREEN.value, 2)
1790+
self.assertEqual(CoolestColor.BLUE.value, 3)
1791+
self.assertEqual(str(CoolestColor.BLUE), 'blue')
1792+
self.assertEqual(CoolestColor.RED.behavior(), 'booyah')
1793+
self.assertEqual(CoolestColor.RED.social(), "what's up?")
1794+
class ConfusedColor(StrMixin, AnotherEnum, SomeEnum):
1795+
RED = auto()
1796+
GREEN = auto()
1797+
BLUE = auto()
1798+
self.assertEqual(ConfusedColor.RED.value, 1)
1799+
self.assertEqual(ConfusedColor.GREEN.value, 2)
1800+
self.assertEqual(ConfusedColor.BLUE.value, 3)
1801+
self.assertEqual(str(ConfusedColor.BLUE), 'blue')
1802+
self.assertEqual(ConfusedColor.RED.behavior(), 'nuhuh!')
1803+
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
1804+
class ReformedColor(StrMixin, IntEnum, SomeEnum, AnotherEnum):
1805+
RED = auto()
1806+
GREEN = auto()
1807+
BLUE = auto()
1808+
self.assertEqual(ReformedColor.RED.value, 1)
1809+
self.assertEqual(ReformedColor.GREEN.value, 2)
1810+
self.assertEqual(ReformedColor.BLUE.value, 3)
1811+
self.assertEqual(str(ReformedColor.BLUE), 'blue')
1812+
self.assertEqual(ReformedColor.RED.behavior(), 'booyah')
1813+
self.assertEqual(ConfusedColor.RED.social(), "what's up?")
1814+
self.assertTrue(issubclass(ReformedColor, int))
1815+
17041816

17051817
class TestOrder(unittest.TestCase):
17061818

@@ -2064,6 +2176,49 @@ class Bizarre(Flag):
20642176
d = 6
20652177
self.assertEqual(repr(Bizarre(7)), '<Bizarre.d|c|b: 7>')
20662178

2179+
def test_multiple_mixin(self):
2180+
class AllMixin:
2181+
@classproperty
2182+
def ALL(cls):
2183+
members = list(cls)
2184+
all_value = None
2185+
if members:
2186+
all_value = members[0]
2187+
for member in members[1:]:
2188+
all_value |= member
2189+
cls.ALL = all_value
2190+
return all_value
2191+
class StrMixin:
2192+
def __str__(self):
2193+
return self._name_.lower()
2194+
class Color(AllMixin, Flag):
2195+
RED = auto()
2196+
GREEN = auto()
2197+
BLUE = auto()
2198+
self.assertEqual(Color.RED.value, 1)
2199+
self.assertEqual(Color.GREEN.value, 2)
2200+
self.assertEqual(Color.BLUE.value, 4)
2201+
self.assertEqual(Color.ALL.value, 7)
2202+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
2203+
class Color(AllMixin, StrMixin, Flag):
2204+
RED = auto()
2205+
GREEN = auto()
2206+
BLUE = auto()
2207+
self.assertEqual(Color.RED.value, 1)
2208+
self.assertEqual(Color.GREEN.value, 2)
2209+
self.assertEqual(Color.BLUE.value, 4)
2210+
self.assertEqual(Color.ALL.value, 7)
2211+
self.assertEqual(str(Color.BLUE), 'blue')
2212+
class Color(StrMixin, AllMixin, Flag):
2213+
RED = auto()
2214+
GREEN = auto()
2215+
BLUE = auto()
2216+
self.assertEqual(Color.RED.value, 1)
2217+
self.assertEqual(Color.GREEN.value, 2)
2218+
self.assertEqual(Color.BLUE.value, 4)
2219+
self.assertEqual(Color.ALL.value, 7)
2220+
self.assertEqual(str(Color.BLUE), 'blue')
2221+
20672222
@support.reap_threads
20682223
def test_unique_composite(self):
20692224
# override __eq__ to be identity only
@@ -2439,6 +2594,49 @@ def test_bool(self):
24392594
for f in Open:
24402595
self.assertEqual(bool(f.value), bool(f))
24412596

2597+
def test_multiple_mixin(self):
2598+
class AllMixin:
2599+
@classproperty
2600+
def ALL(cls):
2601+
members = list(cls)
2602+
all_value = None
2603+
if members:
2604+
all_value = members[0]
2605+
for member in members[1:]:
2606+
all_value |= member
2607+
cls.ALL = all_value
2608+
return all_value
2609+
class StrMixin:
2610+
def __str__(self):
2611+
return self._name_.lower()
2612+
class Color(AllMixin, IntFlag):
2613+
RED = auto()
2614+
GREEN = auto()
2615+
BLUE = auto()
2616+
self.assertEqual(Color.RED.value, 1)
2617+
self.assertEqual(Color.GREEN.value, 2)
2618+
self.assertEqual(Color.BLUE.value, 4)
2619+
self.assertEqual(Color.ALL.value, 7)
2620+
self.assertEqual(str(Color.BLUE), 'Color.BLUE')
2621+
class Color(AllMixin, StrMixin, IntFlag):
2622+
RED = auto()
2623+
GREEN = auto()
2624+
BLUE = auto()
2625+
self.assertEqual(Color.RED.value, 1)
2626+
self.assertEqual(Color.GREEN.value, 2)
2627+
self.assertEqual(Color.BLUE.value, 4)
2628+
self.assertEqual(Color.ALL.value, 7)
2629+
self.assertEqual(str(Color.BLUE), 'blue')
2630+
class Color(StrMixin, AllMixin, IntFlag):
2631+
RED = auto()
2632+
GREEN = auto()
2633+
BLUE = auto()
2634+
self.assertEqual(Color.RED.value, 1)
2635+
self.assertEqual(Color.GREEN.value, 2)
2636+
self.assertEqual(Color.BLUE.value, 4)
2637+
self.assertEqual(Color.ALL.value, 7)
2638+
self.assertEqual(str(Color.BLUE), 'blue')
2639+
24422640
@support.reap_threads
24432641
def test_unique_composite(self):
24442642
# override __eq__ to be identity only
@@ -2524,6 +2722,7 @@ class Sillier(IntEnum):
25242722
value = 4
25252723

25262724

2725+
25272726
expected_help_output_with_docs = """\
25282727
Help on class Color in module %s:
25292728
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)