diff --git a/Lib/enum.py b/Lib/enum.py --- a/Lib/enum.py +++ b/Lib/enum.py @@ -778,21 +778,21 @@ class IntFlag(int, Flag): need_to_create.append(flag_value) if extra_flags == -flag_value: extra_flags = 0 else: extra_flags ^= flag_value for value in reversed(need_to_create): # construct singleton pseudo-members pseudo_member = int.__new__(cls, value) pseudo_member._name_ = None pseudo_member._value_ = value - cls._value2member_map_[value] = pseudo_member + pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member) return pseudo_member def __or__(self, other): if not isinstance(other, (self.__class__, int)): return NotImplemented result = self.__class__(self._value_ | self.__class__(other)._value_) return result def __and__(self, other): if not isinstance(other, (self.__class__, int)): @@ -828,32 +828,35 @@ def unique(enumeration): ["%s -> %s" % (alias, name) for (alias, name) in duplicates]) raise ValueError('duplicate values found in %r: %s' % (enumeration, alias_details)) return enumeration def _decompose(flag, value): """Extract all members from the value.""" # _decompose is only called if the value is not named not_covered = value negative = value < 0 + # issue29167: wrap accesses to _value2member_map_ in a list to avoid race + # conditions between iterating over it and having more psuedo- + # members added to it if negative: # only check for named flags flags_to_check = [ (m, v) - for v, m in flag._value2member_map_.items() + for v, m in list(flag._value2member_map_.items()) if m.name is not None ] else: # check for named flags and powers-of-two flags flags_to_check = [ (m, v) - for v, m in flag._value2member_map_.items() + for v, m in list(flag._value2member_map_.items()) if m.name is not None or _power_of_two(v) ] members = [] for member, member_value in flags_to_check: if member_value and member_value & value == member_value: members.append(member) not_covered &= ~member_value if not members and value in flag._value2member_map_: members.append(flag._value2member_map_[value]) members.sort(key=lambda m: m._value_, reverse=True) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -1,19 +1,24 @@ import enum import inspect import pydoc import unittest from collections import OrderedDict from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support +try: + import threading +except ImportError: + threading = None + # for pickle tests try: class Stooges(Enum): LARRY = 1 CURLY = 2 MOE = 3 except Exception as exc: Stooges = exc @@ -1976,20 +1981,59 @@ class TestFlag(unittest.TestCase): third = auto() self.assertEqual([Dupes.first, Dupes.second, Dupes.third], list(Dupes)) def test_bizarre(self): class Bizarre(Flag): b = 3 c = 4 d = 6 self.assertEqual(repr(Bizarre(7)), '') + @unittest.skipUnless(threading, 'Threading required for this test.') + @support.reap_threads + def test_unique_composite(self): + # override __eq__ to be identity only + class TestFlag(IntFlag): + one = auto() + two = auto() + three = auto() + four = auto() + five = auto() + six = auto() + seven = auto() + eight = auto() + def __eq__(self, other): + return self is other + def __hash__(self): + return hash(self._value_) + # have multiple threads competing to complete the composite members + seen = set() + failed = False + def cycle_enum(): + nonlocal failed + try: + for i in range(256): + seen.add(TestFlag(i)) + except Exception: + failed = True + threads = [ + threading.Thread(target=cycle_enum) + for _ in range(8) + ] + with support.start_threads(threads): + pass + # check that only 248 members were created + self.assertFalse( + failed, + 'at least one thread failed while creating composite members') + self.assertEqual(256, len(seen), 'too many composite members created') + class TestIntFlag(unittest.TestCase): """Tests of the IntFlags.""" class Perm(IntFlag): X = 1 << 0 W = 1 << 1 R = 1 << 2 class Open(IntFlag): @@ -2268,20 +2312,60 @@ class TestIntFlag(unittest.TestCase): self.assertFalse(X in RW) def test_bool(self): Perm = self.Perm for f in Perm: self.assertTrue(f) Open = self.Open for f in Open: self.assertEqual(bool(f.value), bool(f)) + @unittest.skipUnless(threading, 'Threading required for this test.') + @support.reap_threads + def test_unique_composite(self): + # override __eq__ to be identity only + class TestFlag(IntFlag): + one = auto() + two = auto() + three = auto() + four = auto() + five = auto() + six = auto() + seven = auto() + eight = auto() + def __eq__(self, other): + return self is other + def __hash__(self): + return hash(self._value_) + # have multiple threads competing to complete the composite members + seen = set() + failed = False + def cycle_enum(): + nonlocal failed + try: + for i in range(256): + seen.add(TestFlag(i)) + except Exception: + failed = True + threads = [ + threading.Thread(target=cycle_enum) + for _ in range(8) + ] + with support.start_threads(threads): + pass + # check that only 248 members were created + self.assertFalse( + failed, + 'at least one thread failed while creating composite members') + self.assertEqual(256, len(seen), 'too many composite members created') + + class TestUnique(unittest.TestCase): def test_unique_clean(self): @unique class Clean(Enum): one = 1 two = 'dos' tres = 4.0 @unique class Cleaner(IntEnum): @@ -2477,30 +2561,32 @@ class MiscTestCase(unittest.TestCase): CONVERT_TEST_NAME_D = 5 CONVERT_TEST_NAME_C = 5 CONVERT_TEST_NAME_B = 5 CONVERT_TEST_NAME_A = 5 # This one should sort first. CONVERT_TEST_NAME_E = 5 CONVERT_TEST_NAME_F = 5 class TestIntEnumConvert(unittest.TestCase): def test_convert_value_lookup_priority(self): test_type = enum.IntEnum._convert( - 'UnittestConvert', 'test.test_enum', + 'UnittestConvert', + ('test.test_enum', '__main__')[__name__=='__main__'], filter=lambda x: x.startswith('CONVERT_TEST_')) # We don't want the reverse lookup value to vary when there are # multiple possible names for a given value. It should always # report the first lexigraphical name in that case. self.assertEqual(test_type(5).name, 'CONVERT_TEST_NAME_A') def test_convert(self): test_type = enum.IntEnum._convert( - 'UnittestConvert', 'test.test_enum', + 'UnittestConvert', + ('test.test_enum', '__main__')[__name__=='__main__'], filter=lambda x: x.startswith('CONVERT_TEST_')) # Ensure that test_type has all of the desired names and values. self.assertEqual(test_type.CONVERT_TEST_NAME_F, test_type.CONVERT_TEST_NAME_A) self.assertEqual(test_type.CONVERT_TEST_NAME_B, 5) self.assertEqual(test_type.CONVERT_TEST_NAME_C, 5) self.assertEqual(test_type.CONVERT_TEST_NAME_D, 5) self.assertEqual(test_type.CONVERT_TEST_NAME_E, 5) # Ensure that test_type only picked up names matching the filter. self.assertEqual([name for name in dir(test_type)