From a865d7621d516354620c9d12bd9333130f093b70 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Thu, 10 Jun 2021 13:30:41 -0700 Subject: [PATCH] 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): GH- some behavior here class Str2Enum(str, Enum): GH- some more behavior here class FinalStrEnum(Str1Enum, Str2Enum): GH- this now works (cherry picked from commit 8a4f0850d75747af8c96ca0e7eef1f5c1abfba25) Co-authored-by: Ethan Furman --- 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 be74796a8a0ceb..f09cb8473dfed7 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -556,7 +556,7 @@ def _get_mixins_(class_name, bases): return object, Enum def _find_data_type(bases): - data_types = [] + data_types = set() for chain in bases: candidate = None for base in chain.__mro__: @@ -564,19 +564,19 @@ def _find_data_type(bases): 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 fc2d61d59cd4be..e8715ba34552b5 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -2072,6 +2072,53 @@ def __new__(cls, value): 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 00000000000000..954a803fe25c18 --- /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.