From a6beb03a359daa5674381f679407bbbc510a9338 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 5 Jul 2023 14:34:19 -0700 Subject: [PATCH 1/6] Fix flag mask inversion when unnamed flags exist. For example: class Flag(enum.Flag): A = 0x01 B = 0x02 MASK = 0xff ~Flag.MASK is Flag(0) --- Lib/enum.py | 9 +- Lib/test/test_enum.py | 120 ++++++++++-------- ...-07-05-14-34-10.gh-issue-105497.HU5u89.rst | 1 + 3 files changed, 67 insertions(+), 63 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-07-05-14-34-10.gh-issue-105497.HU5u89.rst diff --git a/Lib/enum.py b/Lib/enum.py index 47e31b17c2a495..4f034a555dbe26 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1515,14 +1515,7 @@ def __xor__(self, other): def __invert__(self): if self._inverted_ is None: - if self._boundary_ is KEEP: - # use all bits - self._inverted_ = self.__class__(~self._value_) - else: - # use canonical bits (i.e. calculate flags not in this member) - self._inverted_ = self.__class__(self._singles_mask_ ^ self._value_) - if isinstance(self._inverted_, self.__class__): - self._inverted_._inverted_ = self + self._inverted_ = self.__class__(self._singles_mask_ & ~self._value_) return self._inverted_ __rand__ = __and__ diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 54b7d18d454151..9723ff8d4cb1c2 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -818,6 +818,71 @@ def test_default_missing_with_wrong_type_value(self): self.MainEnum('RED') self.assertIs(ctx.exception.__context__, None) + def test_closed_invert_expectations(self): + class ClosedAB(self.enum_type): + A = 1 + B = 2 + MASK = 3 + A, B = ClosedAB + AB_MASK = ClosedAB.MASK + # + self.assertIs(~A, B) + self.assertIs(~B, A) + self.assertIs(~(A|B), ClosedAB(0)) + self.assertIs(~AB_MASK, ClosedAB(0)) + self.assertIs(~ClosedAB(0), (A|B)) + # + class ClosedXYZ(self.enum_type): + X = 4 + Y = 2 + Z = 1 + MASK = 7 + X, Y, Z = ClosedXYZ + XYZ_MASK = ClosedXYZ.MASK + # + self.assertIs(~X, Y|Z) + self.assertIs(~Y, X|Z) + self.assertIs(~Z, X|Y) + self.assertIs(~(X|Y), Z) + self.assertIs(~(X|Z), Y) + self.assertIs(~(Y|Z), X) + self.assertIs(~(X|Y|Z), ClosedXYZ(0)) + self.assertIs(~XYZ_MASK, ClosedXYZ(0)) + self.assertIs(~ClosedXYZ(0), (X|Y|Z)) + + def test_open_invert_expectations(self): + class OpenAB(self.enum_type): + A = 1 + B = 2 + MASK = 255 + A, B = OpenAB + AB_MASK = OpenAB.MASK + # + self.assertIs(~A, B) + self.assertIs(~B, A) + self.assertIs(~(A|B), OpenAB(0)) + self.assertIs(~AB_MASK, OpenAB(0)) + self.assertIs(~OpenAB(0), (A|B)) + # + class OpenXYZ(self.enum_type): + X = 4 + Y = 2 + Z = 1 + MASK = 31 + X, Y, Z = OpenXYZ + XYZ_MASK = OpenXYZ.MASK + # + self.assertIs(~X, Y|Z) + self.assertIs(~Y, X|Z) + self.assertIs(~Z, X|Y) + self.assertIs(~(X|Y), Z) + self.assertIs(~(X|Z), Y) + self.assertIs(~(Y|Z), X) + self.assertIs(~(X|Y|Z), OpenXYZ(0)) + self.assertIs(~XYZ_MASK, OpenXYZ(0)) + self.assertTrue(~OpenXYZ(0), (X|Y|Z)) + + class TestPlainEnum(_EnumTests, _PlainOutputTests, unittest.TestCase): enum_type = Enum @@ -3045,33 +3110,6 @@ class Color(Flag): WHITE = RED|GREEN|BLUE BLANCO = RED|GREEN|BLUE - class Complete(Flag): - A = 0x01 - B = 0x02 - - class Partial(Flag): - A = 0x01 - B = 0x02 - MASK = 0xff - - class CompleteInt(IntFlag): - A = 0x01 - B = 0x02 - - class PartialInt(IntFlag): - A = 0x01 - B = 0x02 - MASK = 0xff - - class CompleteIntStrict(IntFlag, boundary=STRICT): - A = 0x01 - B = 0x02 - - class PartialIntStrict(IntFlag, boundary=STRICT): - A = 0x01 - B = 0x02 - MASK = 0xff - def test_or(self): Perm = self.Perm for i in Perm: @@ -3115,34 +3153,6 @@ def test_xor(self): self.assertIs(Open.RO ^ Open.CE, Open.CE) self.assertIs(Open.CE ^ Open.CE, Open.RO) - def test_invert(self): - Perm = self.Perm - RW = Perm.R | Perm.W - RX = Perm.R | Perm.X - WX = Perm.W | Perm.X - RWX = Perm.R | Perm.W | Perm.X - values = list(Perm) + [RW, RX, WX, RWX, Perm(0)] - for i in values: - self.assertIs(type(~i), Perm) - self.assertEqual(~~i, i) - for i in Perm: - self.assertIs(~~i, i) - Open = self.Open - self.assertIs(Open.WO & ~Open.WO, Open.RO) - self.assertIs((Open.WO|Open.CE) & ~Open.WO, Open.CE) - Complete = self.Complete - self.assertIs(~Complete.A, Complete.B) - Partial = self.Partial - self.assertIs(~Partial.A, Partial.B) - CompleteInt = self.CompleteInt - self.assertIs(~CompleteInt.A, CompleteInt.B) - PartialInt = self.PartialInt - self.assertIs(~PartialInt.A, PartialInt(254)) - CompleteIntStrict = self.CompleteIntStrict - self.assertIs(~CompleteIntStrict.A, CompleteIntStrict.B) - PartialIntStrict = self.PartialIntStrict - self.assertIs(~PartialIntStrict.A, PartialIntStrict.B) - def test_bool(self): Perm = self.Perm for f in Perm: diff --git a/Misc/NEWS.d/next/Library/2023-07-05-14-34-10.gh-issue-105497.HU5u89.rst b/Misc/NEWS.d/next/Library/2023-07-05-14-34-10.gh-issue-105497.HU5u89.rst new file mode 100644 index 00000000000000..f4f2db08f73f50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-07-05-14-34-10.gh-issue-105497.HU5u89.rst @@ -0,0 +1 @@ +Fix flag mask inversion when unnamed flags exist. From 758de5f51d97189386af4ebba989cc7592eb7a0a Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 5 Jul 2023 16:49:08 -0700 Subject: [PATCH 2/6] EJECT and KEEP flags (IntEnum is KEEP) use direct value. --- Lib/enum.py | 5 ++++- Lib/test/test_enum.py | 46 ++++++++++++++++++++++++++++++------------- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 4f034a555dbe26..202f0da028bdfe 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1515,7 +1515,10 @@ def __xor__(self, other): def __invert__(self): if self._inverted_ is None: - self._inverted_ = self.__class__(self._singles_mask_ & ~self._value_) + if self._boundary_ in (EJECT, KEEP): + self._inverted_ = self.__class__(~self._value_) + else: + self._inverted_ = self.__class__(self._singles_mask_ & ~self._value_) return self._inverted_ __rand__ = __and__ diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 9723ff8d4cb1c2..adb1e0e52c5485 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -858,11 +858,18 @@ class OpenAB(self.enum_type): A, B = OpenAB AB_MASK = OpenAB.MASK # - self.assertIs(~A, B) - self.assertIs(~B, A) - self.assertIs(~(A|B), OpenAB(0)) - self.assertIs(~AB_MASK, OpenAB(0)) - self.assertIs(~OpenAB(0), (A|B)) + if OpenAB._boundary_ in (EJECT, KEEP): + self.assertIs(~A, OpenAB(254)) + self.assertIs(~B, OpenAB(253)) + self.assertIs(~(A|B), OpenAB(252)) + self.assertIs(~AB_MASK, OpenAB(0)) + self.assertIs(~OpenAB(0), AB_MASK) + else: + self.assertIs(~A, B) + self.assertIs(~B, A) + self.assertIs(~(A|B), OpenAB(0)) + self.assertIs(~AB_MASK, OpenAB(0)) + self.assertIs(~OpenAB(0), (A|B)) # class OpenXYZ(self.enum_type): X = 4 @@ -872,15 +879,26 @@ class OpenXYZ(self.enum_type): X, Y, Z = OpenXYZ XYZ_MASK = OpenXYZ.MASK # - self.assertIs(~X, Y|Z) - self.assertIs(~Y, X|Z) - self.assertIs(~Z, X|Y) - self.assertIs(~(X|Y), Z) - self.assertIs(~(X|Z), Y) - self.assertIs(~(Y|Z), X) - self.assertIs(~(X|Y|Z), OpenXYZ(0)) - self.assertIs(~XYZ_MASK, OpenXYZ(0)) - self.assertTrue(~OpenXYZ(0), (X|Y|Z)) + if OpenXYZ._boundary_ in (EJECT, KEEP): + self.assertIs(~X, OpenXYZ(27)) + self.assertIs(~Y, OpenXYZ(29)) + self.assertIs(~Z, OpenXYZ(30)) + self.assertIs(~(X|Y), OpenXYZ(25)) + self.assertIs(~(X|Z), OpenXYZ(26)) + self.assertIs(~(Y|Z), OpenXYZ(28)) + self.assertIs(~(X|Y|Z), OpenXYZ(24)) + self.assertIs(~XYZ_MASK, OpenXYZ(0)) + self.assertTrue(~OpenXYZ(0), XYZ_MASK) + else: + self.assertIs(~X, Y|Z) + self.assertIs(~Y, X|Z) + self.assertIs(~Z, X|Y) + self.assertIs(~(X|Y), Z) + self.assertIs(~(X|Z), Y) + self.assertIs(~(Y|Z), X) + self.assertIs(~(X|Y|Z), OpenXYZ(0)) + self.assertIs(~XYZ_MASK, OpenXYZ(0)) + self.assertTrue(~OpenXYZ(0), (X|Y|Z)) class TestPlainEnum(_EnumTests, _PlainOutputTests, unittest.TestCase): From 7e60935156dd715f37eeb09a198fa65c0c8ef71e Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Mon, 7 Apr 2025 19:32:50 -0700 Subject: [PATCH 3/6] correct Flag inversion to only flip flag bits IntFlag will flip all bits -- this only makes a difference in flag sets with missing values. --- Lib/enum.py | 5 ++++- Lib/test/test_enum.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index 202f0da028bdfe..573340d7266f3c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1398,7 +1398,10 @@ def _missing_(cls, value): ) if value < 0: neg_value = value - value = all_bits + 1 + value + if cls._boundary_ in (EJECT, KEEP): + value = all_bits + 1 + value + else: + value = singles_mask & value # get members and unknown unknown = value & ~flag_mask aliases = value & ~singles_mask diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index adb1e0e52c5485..3dd28275d6da83 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -864,12 +864,18 @@ class OpenAB(self.enum_type): self.assertIs(~(A|B), OpenAB(252)) self.assertIs(~AB_MASK, OpenAB(0)) self.assertIs(~OpenAB(0), AB_MASK) + self.assertIs(OpenAB(~4), OpenAB(251)) else: self.assertIs(~A, B) self.assertIs(~B, A) + self.assertIs(OpenAB(~1), B) + self.assertIs(OpenAB(~2), A) self.assertIs(~(A|B), OpenAB(0)) self.assertIs(~AB_MASK, OpenAB(0)) self.assertIs(~OpenAB(0), (A|B)) + self.assertIs(OpenAB(~3), OpenAB(0)) + self.assertIs(OpenAB(~4), OpenAB(3)) + self.assertIs(OpenAB(~33), B) # class OpenXYZ(self.enum_type): X = 4 @@ -893,6 +899,9 @@ class OpenXYZ(self.enum_type): self.assertIs(~X, Y|Z) self.assertIs(~Y, X|Z) self.assertIs(~Z, X|Y) + self.assertIs(OpenXYZ(~4), Y|Z) + self.assertIs(OpenXYZ(~2), X|Z) + self.assertIs(OpenXYZ(~1), X|Y) self.assertIs(~(X|Y), Z) self.assertIs(~(X|Z), Y) self.assertIs(~(Y|Z), X) @@ -3222,6 +3231,8 @@ class SkipFlag(enum.Flag): C = 4 | B # self.assertTrue(SkipFlag.C in (SkipFlag.A|SkipFlag.C)) + self.assertTrue(SkipFlag.B in SkipFlag.C) + self.assertIs(SkipFlag(~1), SkipFlag.B) self.assertRaisesRegex(ValueError, 'SkipFlag.. invalid value 42', SkipFlag, 42) # class SkipIntFlag(enum.IntFlag): @@ -3230,6 +3241,8 @@ class SkipIntFlag(enum.IntFlag): C = 4 | B # self.assertTrue(SkipIntFlag.C in (SkipIntFlag.A|SkipIntFlag.C)) + self.assertTrue(SkipIntFlag.B in SkipIntFlag.C) + self.assertIs(SkipIntFlag(~1), SkipIntFlag.B|SkipIntFlag.C) self.assertEqual(SkipIntFlag(42).value, 42) # class MethodHint(Flag): From 6ecb9b40c95698c469f6389f99e6ebfb3b84a213 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 8 Apr 2025 07:04:37 -0700 Subject: [PATCH 4/6] correct negative assigned values in flags negative values are no longer used as-is, but become inverted; i.e. class Y(self.enum_type): A = auto() B = auto() C = ~A # aka ~1 aka 0b1 110 (from enum.bin()) aka 6 D = auto() assert Y.C. is Y.B|Y.D --- Lib/enum.py | 14 +++++++++++++- Lib/test/test_enum.py | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index 573340d7266f3c..58542a96913ab3 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -560,7 +560,7 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k # now set the __repr__ for the value classdict['_value_repr_'] = metacls._find_data_repr_(cls, bases) # - # Flag structures (will be removed if final class is not a Flag + # Flag structures (will be removed if final class is not a Flag) classdict['_boundary_'] = ( boundary or getattr(first_enum, '_boundary_', None) @@ -569,6 +569,18 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k classdict['_singles_mask_'] = 0 classdict['_all_bits_'] = 0 classdict['_inverted_'] = None + # check for negative flag values and invert if found (using _proto_members) + if Flag is not None and bases and issubclass(bases[-1], Flag): + bits = 0 + inverted = [] + for n in member_names: + p = classdict[n] + if p.value < 0: + inverted.append(p) + else: + bits |= p.value + for p in inverted: + p.value = bits & p.value try: exc = None enum_class = super().__new__(metacls, cls, bases, classdict, **kwds) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 3dd28275d6da83..60be45407b281d 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -909,6 +909,27 @@ class OpenXYZ(self.enum_type): self.assertIs(~XYZ_MASK, OpenXYZ(0)) self.assertTrue(~OpenXYZ(0), (X|Y|Z)) + def test_assigned_negative_value(self): + class X(self.enum_type): + A = auto() + B = auto() + C = A | B + D = ~A + self.assertEqual(list(X), [X.A, X.B]) + self.assertIs(~X.A, X.B) + self.assertIs(X.D, X.B) + self.assertEqual(X.D.value, 2) + # + class Y(self.enum_type): + A = auto() + B = auto() + C = A | B + D = ~A + E = auto() + self.assertEqual(list(Y), [Y.A, Y.B, Y.E]) + self.assertIs(~Y.A, Y.B|Y.E) + self.assertIs(Y.D, Y.B|Y.E) + self.assertEqual(Y.D.value, 6) class TestPlainEnum(_EnumTests, _PlainOutputTests, unittest.TestCase): enum_type = Enum @@ -4273,6 +4294,8 @@ class Color(Flag): BLUE = 4 WHITE = -1 # no error means success + self.assertEqual(list(Color.WHITE), [Color.RED, Color.GREEN, Color.BLUE]) + self.assertEqual(Color.WHITE.value, 7) class TestInternals(unittest.TestCase): From ffc19105e059d9101fc5611c3fa90db80c48d746 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 8 Apr 2025 07:25:18 -0700 Subject: [PATCH 5/6] add blurb --- .../Library/2025-04-08-07-25-10.gh-issue-107583.JGfbhq.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-08-07-25-10.gh-issue-107583.JGfbhq.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-08-07-25-10.gh-issue-107583.JGfbhq.rst b/Misc/NEWS.d/next/Library/2025-04-08-07-25-10.gh-issue-107583.JGfbhq.rst new file mode 100644 index 00000000000000..4235612627341b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-08-07-25-10.gh-issue-107583.JGfbhq.rst @@ -0,0 +1,4 @@ +Fix :class:`!Flag` inversion when flag set has missing values +(:class:`!IntFlag` still flips all bits); fix negative assigned values +during flag creation (both :class:`!Flag` and :class:`!IntFlag` ignore +missing values). From 0a4dd5b33b6b53124f9697a10866e6c89ee44d61 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Tue, 8 Apr 2025 11:07:12 -0700 Subject: [PATCH 6/6] handle values of None or tuples --- Lib/enum.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index b6ec32b98e450c..eb14c7842ea4f4 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -550,12 +550,23 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k inverted = [] for n in member_names: p = classdict[n] - if p.value < 0: - inverted.append(p) - else: - bits |= p.value + if isinstance(p.value, int): + if p.value < 0: + inverted.append(p) + else: + bits |= p.value + elif p.value is None: + pass + elif isinstance(p.value, tuple) and p.value and isinstance(p.value[0], int): + if p.value[0] < 0: + inverted.append(p) + else: + bits |= p.value[0] for p in inverted: - p.value = bits & p.value + if isinstance(p.value, int): + p.value = bits & p.value + else: + p.value = (bits & p.value[0], ) + p.value[1:] try: classdict['_%s__in_progress' % cls] = True enum_class = super().__new__(metacls, cls, bases, classdict, **kwds)