From 8a4f0850d75747af8c96ca0e7eef1f5c1abfba25 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 10 Jun 2021 13:30:41 -0700 Subject: bpo-44356: [Enum] allow multiple data-type mixins if they are all the same (GH-26649) This enables, for example, two base Enums to both inherit from `str`, and then both be mixed into the same final Enum: class Str1Enum(str, Enum): # some behavior here class Str2Enum(str, Enum): # some more behavior here class FinalStrEnum(Str1Enum, Str2Enum): # this now works --- Lib/enum.py | 8 ++-- Lib/test/test_enum.py | 47 ++++++++++++++++++++++ .../2021-06-10-08-35-38.bpo-44356.6oDFhO.rst | 1 + 3 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-06-10-08-35-38.bpo-44356.6oDFhO.rst diff --git a/Lib/enum.py b/Lib/enum.py index f74cc8c..54633d8 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -819,7 +819,7 @@ class EnumType(type): return object, Enum def _find_data_type(bases): - data_types = [] + data_types = set() for chain in bases: candidate = None for base in chain.__mro__: @@ -827,19 +827,19 @@ class EnumType(type): continue elif issubclass(base, Enum): if base._member_type_ is not object: - data_types.append(base._member_type_) + data_types.add(base._member_type_) break elif '__new__' in base.__dict__: if issubclass(base, Enum): continue - data_types.append(candidate or base) + data_types.add(candidate or base) break else: candidate = base if len(data_types) > 1: raise TypeError('%r: too many data types: %r' % (class_name, data_types)) elif data_types: - return data_types[0] + return data_types.pop() else: return None diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 34b190b..40794e3 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2144,6 +2144,53 @@ class TestEnum(unittest.TestCase): return member self.assertEqual(Fee.TEST, 2) + def test_miltuple_mixin_with_common_data_type(self): + class CaseInsensitiveStrEnum(str, Enum): + @classmethod + def _missing_(cls, value): + for member in cls._member_map_.values(): + if member._value_.lower() == value.lower(): + return member + return super()._missing_(value) + # + class LenientStrEnum(str, Enum): + def __init__(self, *args): + self._valid = True + @classmethod + def _missing_(cls, value): + # encountered an unknown value! + # Luckily I'm a LenientStrEnum, so I won't crash just yet. + # You might want to add a new case though. + unknown = cls._member_type_.__new__(cls, value) + unknown._valid = False + unknown._name_ = value.upper() + unknown._value_ = value + cls._member_map_[value] = unknown + return unknown + @property + def valid(self): + return self._valid + # + class JobStatus(CaseInsensitiveStrEnum, LenientStrEnum): + ACTIVE = "active" + PENDING = "pending" + TERMINATED = "terminated" + # + JS = JobStatus + self.assertEqual(list(JobStatus), [JS.ACTIVE, JS.PENDING, JS.TERMINATED]) + self.assertEqual(JS.ACTIVE, 'active') + self.assertEqual(JS.ACTIVE.value, 'active') + self.assertIs(JS('Active'), JS.ACTIVE) + self.assertTrue(JS.ACTIVE.valid) + missing = JS('missing') + self.assertEqual(list(JobStatus), [JS.ACTIVE, JS.PENDING, JS.TERMINATED]) + self.assertEqual(JS.ACTIVE, 'active') + self.assertEqual(JS.ACTIVE.value, 'active') + self.assertIs(JS('Active'), JS.ACTIVE) + self.assertTrue(JS.ACTIVE.valid) + self.assertTrue(isinstance(missing, JS)) + self.assertFalse(missing.valid) + def test_empty_globals(self): # bpo-35717: sys._getframe(2).f_globals['__name__'] fails with KeyError # when using compile and exec because f_globals is empty diff --git a/Misc/NEWS.d/next/Library/2021-06-10-08-35-38.bpo-44356.6oDFhO.rst b/Misc/NEWS.d/next/Library/2021-06-10-08-35-38.bpo-44356.6oDFhO.rst new file mode 100644 index 0000000..954a803 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-10-08-35-38.bpo-44356.6oDFhO.rst @@ -0,0 +1 @@ +[Enum] Allow multiple data-type mixins if they are all the same. -- cgit v0.12