diff --git a/Lib/enum.py b/Lib/enum.py index 7aff36c94ce1dcf..076aa18a02fd20a 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -624,9 +624,13 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k '__invert__' ): if name not in classdict: + # check for mixin overrides before replacing enum_method = getattr(Flag, name) - setattr(enum_class, name, enum_method) - classdict[name] = enum_method + found_method = getattr(enum_class, name) + data_type_method = getattr(member_type, name, None) + if found_method in (enum_method, data_type_method): + setattr(enum_class, name, enum_method) + classdict[name] = enum_method # # replace any other __new__ with our own (as long as Enum is not None, # anyway) -- again, this is to support pickle diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index b05eab43bd9eff1..447f847f33da938 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -4080,6 +4080,39 @@ class NeverEnum(WhereEnum): self.assertFalse(NeverEnum.__dict__.get('_test1', False)) self.assertFalse(NeverEnum.__dict__.get('_test2', False)) + def test_mixin_operator_override(self): + # a mixin's own bitwise-operator overrides must not be clobbered + # by Flag's default __or__/__and__/__xor__/__invert__ -- gh-121291 + class OperatorMixin: + def __or__(self, other): + return 'mixin-or' + def __ror__(self, other): + return 'mixin-ror' + def __invert__(self): + return 'mixin-invert' + class MixedFlag(OperatorMixin, Flag): + A = 1 + B = 2 + self.assertIs(MixedFlag.__or__, OperatorMixin.__or__) + self.assertIs(MixedFlag.__ror__, OperatorMixin.__ror__) + self.assertIs(MixedFlag.__invert__, OperatorMixin.__invert__) + self.assertEqual(MixedFlag.A | MixedFlag.B, 'mixin-or') + self.assertEqual(1 | MixedFlag.A, 'mixin-ror') + self.assertEqual(~MixedFlag.A, 'mixin-invert') + # dunders the mixin didn't override still get Flag's own + self.assertIs(MixedFlag.__and__, Flag.__and__) + self.assertIs(MixedFlag.__xor__, Flag.__xor__) + self.assertIs(MixedFlag.__rand__, Flag.__rand__) + self.assertIs(MixedFlag.__rxor__, Flag.__rxor__) + self.assertEqual(MixedFlag.A & MixedFlag.B, MixedFlag(0)) + # + # a plain (non-mixin) Flag subclass is unaffected + class PlainFlag(Flag): + A = 1 + B = 2 + self.assertIs(PlainFlag.__or__, Flag.__or__) + self.assertEqual(PlainFlag.A | PlainFlag.B, PlainFlag(3)) + class OldTestIntFlag(unittest.TestCase): """Tests of the IntFlags.""" @@ -4564,6 +4597,26 @@ def cycle_enum(): 'at least one thread failed while creating composite members') self.assertEqual(256, len(seen), 'too many composite members created') + def test_mixin_operator_override(self): + # IntFlag's own mixed-in `int` also defines these operators, so the + # fix for gh-121291 must still override `int`'s raw operators with + # Flag's (returning IntFlag instances, not plain ints), while still + # respecting a genuine, separate mixin's override. + Color = self.Color + combined = Color.RED | Color.BLUE + self.assertIs(type(combined), Color) + self.assertEqual(combined, Color.PURPLE) + self.assertEqual(repr(combined), '') + # + class OperatorMixin: + def __or__(self, other): + return 'mixin-or' + class MixedIntFlag(OperatorMixin, IntFlag): + A = 1 + B = 2 + self.assertIs(MixedIntFlag.__or__, OperatorMixin.__or__) + self.assertEqual(MixedIntFlag.A | MixedIntFlag.B, 'mixin-or') + class TestEmptyAndNonLatinStrings(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-08-15-18-16-16.gh-issue-121291.WljPkh.rst b/Misc/NEWS.d/next/Library/2026-08-15-18-16-16.gh-issue-121291.WljPkh.rst new file mode 100644 index 000000000000000..e6d9f4b69ec9e37 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-18-16-16.gh-issue-121291.WljPkh.rst @@ -0,0 +1,4 @@ +:class:`enum.Flag` (and :class:`enum.IntFlag`) subclasses no longer have +a mixin base's own ``__or__``, ``__and__``, ``__xor__``, ``__ror__``, +``__rand__``, ``__rxor__``, or ``__invert__`` override silently replaced +by :class:`~enum.Flag`'s default implementation.