diff --git a/Lib/enum.py b/Lib/enum.py index 461d276eed..127773c9a4 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -4,7 +4,6 @@ from functools import reduce from builtins import property as _bltin_property, bin as _bltin_bin - __all__ = [ 'EnumType', 'EnumMeta', 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', @@ -635,10 +634,47 @@ def __delattr__(cls, attr): super().__delattr__(attr) def __dir__(self): - return ( - ['__class__', '__doc__', '__members__', '__module__'] - + self._member_names_ - ) + # Start off with the desired result for dir(Enum) + cls_dir = {'__class__', '__doc__', '__members__', '__module__'} + add_to_dir = cls_dir.add + enum_dict = Enum.__dict__ + + # We want these always excluded from __dir__ + exclude_from_dir = set(filter(_is_sunder, enum_dict)) + + # We want these added to __dir__ + # if and only if they have been user-overridden + enum_dunders = set(filter(_is_dunder, enum_dict)) + + mro = self.mro() + this_module = globals().values() + first_enum_base = next(base for base in mro if base in this_module) + + # special-case __new__ + if self.__new__ is not first_enum_base.__new__: + add_to_dir('__new__') + + for base in mro: + # Ignore any classes defined in this module + if base is object or base in this_module: + continue + + # Avoid dir() if it's an Enum subclass (infinite recursion otherwise) + # Otherwise, go according to dir() + base_lookup = base.__dict__ if Enum in base.mro() else dir(base) + + for attr_name in base_lookup: + if attr_name == '__new__': + continue + + elif attr_name in enum_dunders: + if getattr(self, attr_name) is not getattr(first_enum_base, attr_name, object()): + add_to_dir(attr_name) + + elif attr_name not in exclude_from_dir: + add_to_dir(attr_name) + + return list(cls_dir) def __getattr__(cls, name): """ diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 7d220871a3..59a0a110d5 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -226,7 +226,7 @@ def wowser(self): return ("Wowser! I'm %s!" % self.name) self.assertEqual( set(dir(Test)), - set(['__class__', '__doc__', '__members__', '__module__', 'this', 'these']), + set(['__class__', '__doc__', '__members__', '__module__', 'this', 'these', 'wowser']), ) self.assertEqual( set(dir(Test.this)), @@ -4154,9 +4154,11 @@ def test_convert(self): 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) + int_dir = set(dir(int)) # Ensure that test_type only picked up names matching the filter. self.assertEqual([name for name in dir(test_type) - if name[0:2] not in ('CO', '__')], + if name[0:2] not in ('CO', '__') + and name not in int_dir], [], msg='Names other than CONVERT_TEST_* found.') @unittest.skipUnless(python_version == (3, 8), @@ -4205,9 +4207,11 @@ def test_convert(self): # Ensure that test_type has all of the desired names and values. self.assertEqual(test_type.CONVERT_STR_TEST_1, 'hello') self.assertEqual(test_type.CONVERT_STR_TEST_2, 'goodbye') + str_dir = set(dir(str)) # Ensure that test_type only picked up names matching the filter. self.assertEqual([name for name in dir(test_type) - if name[0:2] not in ('CO', '__')], + if name[0:2] not in ('CO', '__') + and name not in str_dir], [], msg='Names other than CONVERT_STR_* found.') def test_convert_repr_and_str(self):