summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEthan Furman <ethan@stoneleaf.us>2018-09-21 19:03:09 -0700
committerEthan Furman <ethan@stoneleaf.us>2018-09-21 19:09:16 -0700
commit13ba6d21f19b87f4768217d0989bea4885be0827 (patch)
treedb67aad2c83c8258a1dba466412fc76a27177aa8
parente5fde1f992e94f166415ab96d874ed1d2e0c8004 (diff)
downloadcpython-git-backport-5bdab64-3.7.tar.gz
[3.7] bpo-29577: Enum: mixin classes don't mix well with already mixed Enums (GH-9328)backport-5bdab64-3.7
* bpo-29577: allow multiple mixin classes. (cherry picked from commit 5bdab641da0afd8aa581dfbde4f82d88d337c4b5) Co-authored-by: Ethan Furman <ethan@stoneleaf.us>
-rw-r--r--Doc/library/enum.rst13
-rw-r--r--Lib/enum.py50
-rw-r--r--Lib/test/test_enum.py199
-rw-r--r--Misc/NEWS.d/next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst1
4 files changed, 229 insertions, 34 deletions
diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index a6aa863a1c..81e9766e42 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -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):
diff --git a/Lib/enum.py b/Lib/enum.py
index 69b41fe7cb..4e8a56818b 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -449,37 +449,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)`")
-
- # 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
+ 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
+
+ # 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
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index 4b17228946..6c147d7ca6 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -120,6 +120,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):
@@ -1701,6 +1717,102 @@ class TestEnum(unittest.TestCase):
third = auto()
self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes))
+ 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):
@@ -2064,6 +2176,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
@@ -2439,6 +2594,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
@@ -2524,6 +2722,7 @@ class TestUnique(unittest.TestCase):
value = 4
+
expected_help_output_with_docs = """\
Help on class Color in module %s:
diff --git a/Misc/NEWS.d/next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst b/Misc/NEWS.d/next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst
new file mode 100644
index 0000000000..bd71ac496a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst
@@ -0,0 +1 @@
+Support multiple mixin classes when creating Enums.