Skip to content

Commit 49365bd

Browse files
authored
gh-107538: [Enum] fix handling of inverted/negative values (GH-132273)
* 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) * EJECT and KEEP flags (IntEnum is KEEP) use direct value. * correct Flag inversion to only flip flag bits IntFlag will flip all bits -- this only makes a difference in flag sets with missing values. * 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
1 parent 56c6f04 commit 49365bd

File tree

4 files changed

+70
-2
lines changed

4 files changed

+70
-2
lines changed

Lib/enum.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k
535535
# now set the __repr__ for the value
536536
classdict['_value_repr_'] = metacls._find_data_repr_(cls, bases)
537537
#
538-
# Flag structures (will be removed if final class is not a Flag
538+
# Flag structures (will be removed if final class is not a Flag)
539539
classdict['_boundary_'] = (
540540
boundary
541541
or getattr(first_enum, '_boundary_', None)
@@ -544,6 +544,29 @@ def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **k
544544
classdict['_singles_mask_'] = 0
545545
classdict['_all_bits_'] = 0
546546
classdict['_inverted_'] = None
547+
# check for negative flag values and invert if found (using _proto_members)
548+
if Flag is not None and bases and issubclass(bases[-1], Flag):
549+
bits = 0
550+
inverted = []
551+
for n in member_names:
552+
p = classdict[n]
553+
if isinstance(p.value, int):
554+
if p.value < 0:
555+
inverted.append(p)
556+
else:
557+
bits |= p.value
558+
elif p.value is None:
559+
pass
560+
elif isinstance(p.value, tuple) and p.value and isinstance(p.value[0], int):
561+
if p.value[0] < 0:
562+
inverted.append(p)
563+
else:
564+
bits |= p.value[0]
565+
for p in inverted:
566+
if isinstance(p.value, int):
567+
p.value = bits & p.value
568+
else:
569+
p.value = (bits & p.value[0], ) + p.value[1:]
547570
try:
548571
classdict['_%s__in_progress' % cls] = True
549572
enum_class = super().__new__(metacls, cls, bases, classdict, **kwds)
@@ -1487,7 +1510,10 @@ def _missing_(cls, value):
14871510
)
14881511
if value < 0:
14891512
neg_value = value
1490-
value = all_bits + 1 + value
1513+
if cls._boundary_ in (EJECT, KEEP):
1514+
value = all_bits + 1 + value
1515+
else:
1516+
value = singles_mask & value
14911517
# get members and unknown
14921518
unknown = value & ~flag_mask
14931519
aliases = value & ~singles_mask

Lib/test/test_enum.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,12 +1002,18 @@ class OpenAB(self.enum_type):
10021002
self.assertIs(~(A|B), OpenAB(252))
10031003
self.assertIs(~AB_MASK, OpenAB(0))
10041004
self.assertIs(~OpenAB(0), AB_MASK)
1005+
self.assertIs(OpenAB(~4), OpenAB(251))
10051006
else:
10061007
self.assertIs(~A, B)
10071008
self.assertIs(~B, A)
1009+
self.assertIs(OpenAB(~1), B)
1010+
self.assertIs(OpenAB(~2), A)
10081011
self.assertIs(~(A|B), OpenAB(0))
10091012
self.assertIs(~AB_MASK, OpenAB(0))
10101013
self.assertIs(~OpenAB(0), (A|B))
1014+
self.assertIs(OpenAB(~3), OpenAB(0))
1015+
self.assertIs(OpenAB(~4), OpenAB(3))
1016+
self.assertIs(OpenAB(~33), B)
10111017
#
10121018
class OpenXYZ(self.enum_type):
10131019
X = 4
@@ -1031,13 +1037,38 @@ class OpenXYZ(self.enum_type):
10311037
self.assertIs(~X, Y|Z)
10321038
self.assertIs(~Y, X|Z)
10331039
self.assertIs(~Z, X|Y)
1040+
self.assertIs(OpenXYZ(~4), Y|Z)
1041+
self.assertIs(OpenXYZ(~2), X|Z)
1042+
self.assertIs(OpenXYZ(~1), X|Y)
10341043
self.assertIs(~(X|Y), Z)
10351044
self.assertIs(~(X|Z), Y)
10361045
self.assertIs(~(Y|Z), X)
10371046
self.assertIs(~(X|Y|Z), OpenXYZ(0))
10381047
self.assertIs(~XYZ_MASK, OpenXYZ(0))
10391048
self.assertTrue(~OpenXYZ(0), (X|Y|Z))
10401049

1050+
def test_assigned_negative_value(self):
1051+
class X(self.enum_type):
1052+
A = auto()
1053+
B = auto()
1054+
C = A | B
1055+
D = ~A
1056+
self.assertEqual(list(X), [X.A, X.B])
1057+
self.assertIs(~X.A, X.B)
1058+
self.assertIs(X.D, X.B)
1059+
self.assertEqual(X.D.value, 2)
1060+
#
1061+
class Y(self.enum_type):
1062+
A = auto()
1063+
B = auto()
1064+
C = A | B
1065+
D = ~A
1066+
E = auto()
1067+
self.assertEqual(list(Y), [Y.A, Y.B, Y.E])
1068+
self.assertIs(~Y.A, Y.B|Y.E)
1069+
self.assertIs(Y.D, Y.B|Y.E)
1070+
self.assertEqual(Y.D.value, 6)
1071+
10411072

10421073
class TestPlainEnumClass(_EnumTests, _PlainOutputTests, unittest.TestCase):
10431074
enum_type = Enum
@@ -3680,6 +3711,8 @@ class SkipFlag(enum.Flag):
36803711
C = 4 | B
36813712
#
36823713
self.assertTrue(SkipFlag.C in (SkipFlag.A|SkipFlag.C))
3714+
self.assertTrue(SkipFlag.B in SkipFlag.C)
3715+
self.assertIs(SkipFlag(~1), SkipFlag.B)
36833716
self.assertRaisesRegex(ValueError, 'SkipFlag.. invalid value 42', SkipFlag, 42)
36843717
#
36853718
class SkipIntFlag(enum.IntFlag):
@@ -3688,6 +3721,8 @@ class SkipIntFlag(enum.IntFlag):
36883721
C = 4 | B
36893722
#
36903723
self.assertTrue(SkipIntFlag.C in (SkipIntFlag.A|SkipIntFlag.C))
3724+
self.assertTrue(SkipIntFlag.B in SkipIntFlag.C)
3725+
self.assertIs(SkipIntFlag(~1), SkipIntFlag.B|SkipIntFlag.C)
36913726
self.assertEqual(SkipIntFlag(42).value, 42)
36923727
#
36933728
class MethodHint(Flag):
@@ -4727,6 +4762,8 @@ class Color(Flag):
47274762
BLUE = 4
47284763
WHITE = -1
47294764
# no error means success
4765+
self.assertEqual(list(Color.WHITE), [Color.RED, Color.GREEN, Color.BLUE])
4766+
self.assertEqual(Color.WHITE.value, 7)
47304767

47314768

47324769
class TestInternals(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix flag mask inversion when unnamed flags exist.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :class:`!Flag` inversion when flag set has missing values
2+
(:class:`!IntFlag` still flips all bits); fix negative assigned values
3+
during flag creation (both :class:`!Flag` and :class:`!IntFlag` ignore
4+
missing values).

0 commit comments

Comments
 (0)