From 60ca8354808b5ad529a93fc702a0785f8b57ba79 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 14 Sep 2018 19:42:45 -0700 Subject: [PATCH 1/4] bpo-29577: support multiple mixin classes --- Doc/library/enum.rst | 13 +++- Lib/enum.py | 70 ++++++++++++--------- Lib/test/test_enum.py | 141 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 32 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 6408c01060406b..7d18599f52d29d 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 02405c865b060a..43ed5327b25a40 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -480,37 +480,49 @@ def _get_mixins_(bases): 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__ + 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 + + # check that zero or one concrete data type has been used, and only one + # Enum type has been used, and that that Enum has no members 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 + if len(bases) == 1: + member_type = _find_data_type(bases) or object + first_enum = bases[0] + elif len(bases) == 2: + mixin, first_enum = bases + if not issubclass(first_enum, Enum) or issubclass(mixin, Enum): + raise TypeError("new enumerations must be created as " + "`EnumName([mixin_type, ...] [data_type,] enum_type)`") + # search for a concrete data type in mixin or first_enum + member_type = _find_data_type(bases) or object else: - for base in bases[0].__mro__: - # most common: (IntEnum, int, Enum, object) - # possible: (, , - # , , - # ) - if issubclass(base, Enum): - if first_enum is None: - first_enum = base - else: - if member_type is None: - member_type = base + # more than two bases + *mixins, first_enum = bases + if (not issubclass(first_enum, Enum) + or any([issubclass(m, Enum) for m in mixins]) + ): + raise TypeError("new enumerations must be created as " + "`EnumName([mixin_type, ...] [data_type,] enum_type)`") + *mixins, data_type = mixins + # verify that none of the mixins are a data type + if _find_data_type(mixins): + raise TypeError("new enumerations must be created as " + "`EnumName([mixin_type, ...] [data_type,] enum_type)`") + # check if data type is one, or base Enum has one + member_type = _find_data_type(bases[-2:]) or object + + # finally, double check that we are not subclassing a class with existing + # enumeration members + 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 b8efb835ce745f..4686beeed8ebd8 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -122,6 +122,22 @@ def test_is_dunder(self): '__', '___', '____', '_____',): 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,44 @@ def _missing_(cls, item): 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 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 TestOrder(unittest.TestCase): @@ -2093,6 +2147,49 @@ class Bizarre(Flag): d = 6 self.assertEqual(repr(Bizarre(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 +2565,49 @@ def test_bool(self): 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 +2693,7 @@ class Sillier(IntEnum): value = 4 + expected_help_output_with_docs = """\ Help on class Color in module %s: From bfe0b9a531d28f538bf0c42493a6b3fb723efc05 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 14 Sep 2018 20:01:16 -0700 Subject: [PATCH 2/4] add NEWS entry --- Doc/library/enum.rst | 2 +- .../next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 7d18599f52d29d..702eacd0e98ac2 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -392,7 +392,7 @@ Restricted Enum subclassing 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:: +order of these base classes is:: def EnumName([mix-in, ...,] [data-type,] base-enum): pass 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 00000000000000..bd1493ea059296 --- /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 `Enum`s. From fe119661a15fe077a941e2bcca35b5aeceb6961f Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 21 Sep 2018 01:34:49 -0700 Subject: [PATCH 3/4] allow enum classes as mixins --- Lib/enum.py | 38 ++++++---------------------- Lib/test/test_enum.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 31 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 43ed5327b25a40..0ccb30d428e47b 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -490,37 +490,13 @@ def _find_data_type(bases): continue return base - # check that zero or one concrete data type has been used, and only one - # Enum type has been used, and that that Enum has no members - member_type = first_enum = None - if len(bases) == 1: - member_type = _find_data_type(bases) or object - first_enum = bases[0] - elif len(bases) == 2: - mixin, first_enum = bases - if not issubclass(first_enum, Enum) or issubclass(mixin, Enum): - raise TypeError("new enumerations must be created as " - "`EnumName([mixin_type, ...] [data_type,] enum_type)`") - # search for a concrete data type in mixin or first_enum - member_type = _find_data_type(bases) or object - else: - # more than two bases - *mixins, first_enum = bases - if (not issubclass(first_enum, Enum) - or any([issubclass(m, Enum) for m in mixins]) - ): - raise TypeError("new enumerations must be created as " - "`EnumName([mixin_type, ...] [data_type,] enum_type)`") - *mixins, data_type = mixins - # verify that none of the mixins are a data type - if _find_data_type(mixins): - raise TypeError("new enumerations must be created as " - "`EnumName([mixin_type, ...] [data_type,] enum_type)`") - # check if data type is one, or base Enum has one - member_type = _find_data_type(bases[-2:]) or object - - # finally, double check that we are not subclassing a class with existing - # enumeration members + # 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") diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 4686beeed8ebd8..aadc11fcc49c3b 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1756,6 +1756,14 @@ def MAX(cls): 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() @@ -1783,6 +1791,56 @@ class Color(StrMixin, MaxMixin, Enum): 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): From c5a4d980a62bf557cb296b30860e9f9ee0e1fe07 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Fri, 21 Sep 2018 01:43:57 -0700 Subject: [PATCH 4/4] Update 2018-09-14-20-00-47.bpo-29577.RzwKFD.rst --- .../next/Library/2018-09-14-20-00-47.bpo-29577.RzwKFD.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index bd1493ea059296..bd71ac496a6713 100644 --- 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 @@ -1 +1 @@ -Support multiple mixin classes when creating `Enum`s. +Support multiple mixin classes when creating Enums.