From 0ed36bc3068f9772f5ba1534ab6863ef3df10990 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 29 Oct 2021 16:33:19 +0100 Subject: [PATCH 01/12] bpo-45535: Improve output of Enum ``dir()`` This PR modifies the ``EnumType.__dir__()`` and ``Enum.__dir__()`` to ensure that user-defined methods and methods inherited from mixin classes always show up in the output of `help()`. This change also makes it easier for IDEs to provide auto-completion. Apologies for the big diff on the tests. --- Doc/howto/enum.rst | 7 +- Doc/library/enum.rst | 5 +- Lib/enum.py | 59 +++- Lib/test/test_enum.py | 322 +++++++++++++++--- .../2021-10-29-16-28-06.bpo-45535.n8NiOE.rst | 1 + 5 files changed, 336 insertions(+), 58 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-10-29-16-28-06.bpo-45535.n8NiOE.rst diff --git a/Doc/howto/enum.rst b/Doc/howto/enum.rst index d9cfad972cfa9d..a912fe8d9060fa 100644 --- a/Doc/howto/enum.rst +++ b/Doc/howto/enum.rst @@ -998,11 +998,12 @@ Plain :class:`Enum` classes always evaluate as :data:`True`. """"""""""""""""""""""""""""" If you give your enum subclass extra methods, like the `Planet`_ -class below, those methods will show up in a :func:`dir` of the member, -but not of the class:: +class below, those methods will show up in a :func:`dir` of the member and the +class. Attributes defined in an :func:`__init__` method will only show up in a +:func:`dir` of the member:: >>> dir(Planet) - ['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__'] + ['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__init__', '__members__', '__module__', 'surface_gravity'] >>> dir(Planet.EARTH) ['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value'] diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 86bf705af77e71..7e8b3f314424ec 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -162,7 +162,8 @@ Data Types .. method:: EnumType.__dir__(cls) Returns ``['__class__', '__doc__', '__members__', '__module__']`` and the - names of the members in *cls*:: + names of the members in ``cls``. User-defined methods and methods from + mixin classes will also be included:: >>> dir(Color) ['BLUE', 'GREEN', 'RED', '__class__', '__doc__', '__members__', '__module__'] @@ -260,7 +261,7 @@ Data Types .. method:: Enum.__dir__(self) Returns ``['__class__', '__doc__', '__module__', 'name', 'value']`` and - any public methods defined on *self.__class__*:: + any public methods defined on ``self.__class__`` or a mixin class:: >>> from datetime import date >>> class Weekday(Enum): diff --git a/Lib/enum.py b/Lib/enum.py index 461d276eed862a..3751e265cf832d 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,43 @@ 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 + + # 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() + is_from_this_module = lambda cls: any(cls is thing for thing in this_module) + first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) + + # special-case __new__ + if self.__new__ is not first_enum_base.__new__: + add_to_dir('__new__') + + for cls in mro: + # Ignore any classes defined in this module + if cls is object or is_from_this_module(cls): + continue + + # Avoid dir() if EnumType is the metaclass (infinite recursion otherwise) + # Otherwise, go according to dir() + cls_lookup = cls.__dict__ if isinstance(cls, EnumType) else dir(cls) + + for attr_name in cls_lookup: + # Exclude all sunders from dir(); __new__ is special-cased + if attr_name == '__new__' or _is_sunder(attr_name): + continue + elif attr_name not in enum_dunders: + add_to_dir(attr_name) + elif getattr(self, attr_name) is not getattr(first_enum_base, attr_name, object()): + add_to_dir(attr_name) + + # sort the output before returning it, so that the result is deterministic. + return sorted(cls_dir) def __getattr__(cls, name): """ @@ -985,13 +1017,16 @@ def __dir__(self): """ Returns all members and all public methods """ - added_behavior = [ - m - for cls in self.__class__.mro() - for m in cls.__dict__ - if m[0] != '_' and m not in self._member_map_ - ] + [m for m in self.__dict__ if m[0] != '_'] - return (['__class__', '__doc__', '__module__'] + added_behavior) + cls = type(self) + + filtered_cls_dir = ( + name for name in dir(cls) + if name not in {'__members__', '__init__', '__new__', *cls._member_names_} + ) + + filtered_self_dict = (name for name in self.__dict__ if not name.startswith('_')) + + return sorted({'name', 'value', *filtered_cls_dir, *filtered_self_dict}) def __format__(self, format_spec): """ diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 7d220871a35ccb..0cdecfda5af23d 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -7,9 +7,10 @@ import unittest import threading from collections import OrderedDict +from datetime import date from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum -from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS +from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, _is_sunder, _is_dunder from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -176,6 +177,10 @@ class Season(Enum): WINTER = 4 self.Season = Season + class FloatEnum(float, Enum): + pass + self.FloatEnum = FloatEnum + class Konstants(float, Enum): E = 2.7182818 PI = 3.1415926 @@ -197,65 +202,296 @@ class Directional(str, Enum): SOUTH = 'south' self.Directional = Directional - from datetime import date + class DateEnum(date, Enum): + pass + self.DateEnum = DateEnum + class Holiday(date, Enum): NEW_YEAR = 2013, 1, 1 IDES_OF_MARCH = 2013, 3, 15 self.Holiday = Holiday - def test_dir_on_class(self): - Season = self.Season - self.assertEqual( - set(dir(Season)), - set(['__class__', '__doc__', '__members__', '__module__', - 'SPRING', 'SUMMER', 'AUTUMN', 'WINTER']), - ) - - def test_dir_on_item(self): - Season = self.Season - self.assertEqual( - set(dir(Season.WINTER)), - set(['__class__', '__doc__', '__module__', 'name', 'value']), - ) - - def test_dir_with_added_behavior(self): - class Test(Enum): + class Wowser(Enum): this = 'that' these = 'those' def wowser(self): + """Wowser docstring""" return ("Wowser! I'm %s!" % self.name) - self.assertEqual( - set(dir(Test)), - set(['__class__', '__doc__', '__members__', '__module__', 'this', 'these']), - ) - self.assertEqual( - set(dir(Test.this)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'wowser']), - ) + self.Wowser = Wowser + + class IntWowser(IntEnum): + this = 1 + these = 2 + def wowser(self): + """Wowser docstring""" + return ("Wowser! I'm %s!" % self.name) + self.IntWowser = IntWowser + + class FloatWowser(float, Enum): + this = 3.14 + these = 4.2 + def wowser(self): + """Wowser docstring""" + return ("Wowser! I'm %s!" % self.name) + self.FloatWowser = FloatWowser + + class WowserNoMembers(Enum): + def wowser(self): + return ("Wowser! I'm %s!" % self.name) + self.WowserNoMembers = WowserNoMembers + + class IntWowserNoMembers(IntEnum): + def wowser(self): + return ("Wowser! I'm %s!" % self.name) + self.IntWowserNoMembers = IntWowserNoMembers + + class FloatWowserNoMembers(float, Enum): + def wowser(self): + return ("Wowser! I'm %s!" % self.name) + self.FloatWowserNoMembers = FloatWowserNoMembers + + class EnumWithInit(Enum): + def __init__(self, greeting, farewell): + self.greeting = greeting + self.farewell = farewell + ENGLISH = 'hello', 'goodbye' + GERMAN = 'Guten Morgen', 'Auf Wiedersehen' + def some_method(self): + pass + self.EnumWithInit = EnumWithInit - def test_dir_on_sub_with_behavior_on_super(self): # see issue22506 - class SuperEnum(Enum): + class SuperEnum1(Enum): def invisible(self): return "did you see me?" - class SubEnum(SuperEnum): + class SubEnum1(SuperEnum1): sample = 5 - self.assertEqual( - set(dir(SubEnum.sample)), - set(['__class__', '__doc__', '__module__', 'name', 'value', 'invisible']), - ) + self.SubEnum1 = SubEnum1 - def test_dir_on_sub_with_behavior_including_instance_dict_on_super(self): - # see issue40084 - class SuperEnum(IntEnum): + class SuperEnum2(IntEnum): def __new__(cls, value, description=""): obj = int.__new__(cls, value) obj._value_ = value obj.description = description return obj - class SubEnum(SuperEnum): + class SubEnum2(SuperEnum2): sample = 5 - self.assertTrue({'description'} <= set(dir(SubEnum.sample))) + self.SubEnum2 = SubEnum2 + + def test_dir_basics_for_all_enums(self): + enums_for_tests = ( + # Generic enums in enum.py + Enum, + IntEnum, + StrEnum, + # Generic enums defined outside of enum.py + self.DateEnum, + self.FloatEnum, + # Concrete enums derived from enum.py generics + self.Grades, + self.Season, + # Concrete enums derived from generics defined outside of enum.py + self.Konstants, + self.Holiday, + # Standard enum with added behaviour & members + self.Wowser, + # Mixin-enum-from-enum.py with added behaviour & members + self.IntWowser, + # Mixin-enum-from-oustide-enum.py with added behaviour & members + self.FloatWowser, + # Equivalents of the three immediately above, but with no members + self.WowserNoMembers, + self.IntWowserNoMembers, + self.FloatWowserNoMembers, + # Enum with members and an __init__ method + self.EnumWithInit, + # Special cases to test + self.SubEnum1, + self.SubEnum2 + ) + + for cls in enums_for_tests: + with self.subTest(cls=cls): + cls_dir = dir(cls) + # test that dir is deterministic + self.assertEqual(cls_dir, dir(cls)) + # test that dir is sorted + self.assertEqual(list(cls_dir), sorted(cls_dir)) + # test that there are no dupes in dir + self.assertEqual(len(cls_dir), len(set(cls_dir))) + # test that there are no sunders in dir + self.assertFalse(any(_is_sunder(attr) for attr in cls_dir)) + self.assertNotIn('__new__', cls_dir) + + for attr in ('__class__', '__doc__', '__members__', '__module__'): + with self.subTest(attr=attr): + self.assertIn(attr, cls_dir) + + def test_dir_for_enum_with_members(self): + enums_for_test = ( + # Enum with members + self.Season, + # IntEnum with members + self.Grades, + # Two custom-mixin enums with members + self.Konstants, + self.Holiday, + # several enums-with-added-behaviour and members + self.Wowser, + self.IntWowser, + self.FloatWowser, + # An enum with an __init__ method and members + self.EnumWithInit, + # Special cases to test + self.SubEnum1, + self.SubEnum2 + ) + + for cls in enums_for_test: + cls_dir = dir(cls) + member_names = cls._member_names_ + with self.subTest(cls=cls): + self.assertTrue(all(member_name in cls_dir for member_name in member_names)) + for member in cls: + member_dir = dir(member) + # test that dir is deterministic + self.assertEqual(member_dir, dir(member)) + # test that dir is sorted + self.assertEqual(list(member_dir), sorted(member_dir)) + # test that there are no dupes in dir + self.assertEqual(len(member_dir), len(set(member_dir))) + + for attr_name in cls_dir: + with self.subTest(attr_name=attr_name): + if attr_name in {'__members__', '__init__', '__new__', *member_names}: + self.assertNotIn(attr_name, member_dir) + else: + self.assertIn(attr_name, member_dir) + + self.assertFalse(any(_is_sunder(attr) for attr in member_dir)) + + def test_dir_for_enums_with_added_behaviour(self): + enums_for_test = ( + self.Wowser, + self.IntWowser, + self.FloatWowser, + self.WowserNoMembers, + self.IntWowserNoMembers, + self.FloatWowserNoMembers + ) + + for cls in enums_for_test: + with self.subTest(cls=cls): + self.assertIn('wowser', dir(cls)) + self.assertTrue(all('wowser' in dir(member) for member in cls)) + + def test_help_output_on_enum_members(self): + added_behaviour_enums = ( + self.Wowser, + self.IntWowser, + self.FloatWowser + ) + + for cls in added_behaviour_enums: + with self.subTest(cls=cls): + rendered_doc = pydoc.render_doc(cls.this) + self.assertIn('Wowser docstring', rendered_doc) + if cls in {self.IntWowser, self.FloatWowser}: + self.assertIn('float(self)', rendered_doc) + + def test_dir_for_enum_with_init(self): + EnumWithInit = self.EnumWithInit + + cls_dir = dir(EnumWithInit) + self.assertIn('__init__', cls_dir) + self.assertIn('some_method', cls_dir) + self.assertNotIn('greeting', cls_dir) + self.assertNotIn('farewell', cls_dir) + + member_dir = dir(EnumWithInit.ENGLISH) + self.assertNotIn('__init__', member_dir) + self.assertIn('some_method', member_dir) + self.assertIn('greeting', member_dir) + self.assertIn('farewell', member_dir) + + def test_mixin_dirs(self): + enums_for_test = ( + # generic mixins from enum.py + (IntEnum, int), + (StrEnum, str), + # generic mixins from outside enum.py + (self.FloatEnum, float), + (self.DateEnum, date), + # concrete mixin from enum.py + (self.Grades, int), + # concrete mixin from outside enum.py + (self.Holiday, date), + # concrete mixin from enum.py with added behaviour + (self.IntWowser, int), + # concrete mixin from outside enum.py with added behaviour + (self.FloatWowser, float) + ) + + enum_dict = Enum.__dict__ + enum_dir = dir(Enum) + enum_module_names = enum.__all__ + is_from_enum_module = lambda cls: cls.__name__ in enum_module_names + is_enum_dunder = lambda attr: _is_dunder(attr) and attr in enum_dict + + # General tests + for enum_cls, mixin_cls in enums_for_test: + with self.subTest(enum_cls=enum_cls): + cls_dir = dir(enum_cls) + mixin_dir = dir(mixin_cls) + cls_dict = enum_cls.__dict__ + + first_enum_base = next( + base for base in enum_cls.__mro__ + if is_from_enum_module(base) + ) + + for attr in mixin_dir: + with self.subTest(attr=attr): + if _is_sunder(attr): + # Unlikely, but no harm in testing + self.assertNotIn(attr, cls_dir) + elif attr in ('__class__', '__doc__', '__members__', '__module__'): + self.assertIn(attr, cls_dir) + elif is_enum_dunder(attr): + if is_from_enum_module(enum_cls): + self.assertNotIn(attr, cls_dir) + elif getattr(enum_cls, attr) is getattr(first_enum_base, attr): + self.assertNotIn(attr, cls_dir) + else: + self.assertIn(attr, cls_dir) + else: + self.assertIn(attr, cls_dir) + + # Some specific examples + int_enum_dir = dir(IntEnum) + self.assertIn('imag', int_enum_dir) + self.assertIn('__rfloordiv__', int_enum_dir) + self.assertNotIn('__format__', int_enum_dir) + self.assertNotIn('__hash__', int_enum_dir) + + class OverridesFormatOutsideEnumModule(Enum): + def __format__(self, *args, **kwargs): + return super().__format__(*args, **kwargs) + SOME_MEMBER = 1 + + self.assertIn('__format__', dir(OverridesFormatOutsideEnumModule)) + self.assertIn('__format__', dir(OverridesFormatOutsideEnumModule.SOME_MEMBER)) + + def test_dir_on_sub_with_behavior_on_super(self): + # see issue22506 + self.assertEqual( + set(dir(self.SubEnum1.sample)), + set(['__class__', '__doc__', '__module__', 'name', 'value', 'invisible']), + ) + + def test_dir_on_sub_with_behavior_including_instance_dict_on_super(self): + # see issue40084 + self.assertTrue({'description'} <= set(dir(self.SubEnum2.sample))) def test_enum_in_enum_out(self): Season = self.Season @@ -4154,9 +4390,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_enum_dir = set(dir(IntEnum)) # 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_enum_dir], [], msg='Names other than CONVERT_TEST_* found.') @unittest.skipUnless(python_version == (3, 8), @@ -4205,9 +4443,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_enum_dir = set(dir(StrEnum)) # 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_enum_dir], [], msg='Names other than CONVERT_STR_* found.') def test_convert_repr_and_str(self): diff --git a/Misc/NEWS.d/next/Library/2021-10-29-16-28-06.bpo-45535.n8NiOE.rst b/Misc/NEWS.d/next/Library/2021-10-29-16-28-06.bpo-45535.n8NiOE.rst new file mode 100644 index 00000000000000..bda1b407a0ee0f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-10-29-16-28-06.bpo-45535.n8NiOE.rst @@ -0,0 +1 @@ +Improve output of ``dir()`` with Enums. From bb4c147962ff573b2daa77b3aa8aba25baaa7f03 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 29 Oct 2021 19:27:15 +0100 Subject: [PATCH 02/12] Simplify Enum.__dir__ slightly --- Lib/enum.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 3751e265cf832d..7089b8adaa7f3e 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -1018,15 +1018,9 @@ def __dir__(self): Returns all members and all public methods """ cls = type(self) - - filtered_cls_dir = ( - name for name in dir(cls) - if name not in {'__members__', '__init__', '__new__', *cls._member_names_} - ) - + to_exclude = {'__members__', '__init__', '__new__', *cls._member_names_} filtered_self_dict = (name for name in self.__dict__ if not name.startswith('_')) - - return sorted({'name', 'value', *filtered_cls_dir, *filtered_self_dict}) + return sorted({'name', 'value', *dir(cls), *filtered_self_dict} - to_exclude) def __format__(self, format_spec): """ From 526d88b1264a516ae20a4fe7b0f0231289dd5e3c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 29 Oct 2021 20:24:43 +0100 Subject: [PATCH 03/12] Simplify diff from upstream a little --- Lib/test/test_enum.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 0cdecfda5af23d..a06000a79ed31e 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -7,10 +7,9 @@ import unittest import threading from collections import OrderedDict -from datetime import date from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum -from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, _is_sunder, _is_dunder +from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS from io import StringIO from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL from test import support @@ -202,6 +201,7 @@ class Directional(str, Enum): SOUTH = 'south' self.Directional = Directional + from datetime import date class DateEnum(date, Enum): pass self.DateEnum = DateEnum @@ -320,7 +320,7 @@ def test_dir_basics_for_all_enums(self): # test that there are no dupes in dir self.assertEqual(len(cls_dir), len(set(cls_dir))) # test that there are no sunders in dir - self.assertFalse(any(_is_sunder(attr) for attr in cls_dir)) + self.assertFalse(any(enum._is_sunder(attr) for attr in cls_dir)) self.assertNotIn('__new__', cls_dir) for attr in ('__class__', '__doc__', '__members__', '__module__'): @@ -368,7 +368,7 @@ def test_dir_for_enum_with_members(self): else: self.assertIn(attr_name, member_dir) - self.assertFalse(any(_is_sunder(attr) for attr in member_dir)) + self.assertFalse(any(enum._is_sunder(attr) for attr in member_dir)) def test_dir_for_enums_with_added_behaviour(self): enums_for_test = ( @@ -415,6 +415,8 @@ def test_dir_for_enum_with_init(self): self.assertIn('farewell', member_dir) def test_mixin_dirs(self): + from datetime import date + enums_for_test = ( # generic mixins from enum.py (IntEnum, int), @@ -436,7 +438,7 @@ def test_mixin_dirs(self): enum_dir = dir(Enum) enum_module_names = enum.__all__ is_from_enum_module = lambda cls: cls.__name__ in enum_module_names - is_enum_dunder = lambda attr: _is_dunder(attr) and attr in enum_dict + is_enum_dunder = lambda attr: enum._is_dunder(attr) and attr in enum_dict # General tests for enum_cls, mixin_cls in enums_for_test: @@ -452,7 +454,7 @@ def test_mixin_dirs(self): for attr in mixin_dir: with self.subTest(attr=attr): - if _is_sunder(attr): + if enum._is_sunder(attr): # Unlikely, but no harm in testing self.assertNotIn(attr, cls_dir) elif attr in ('__class__', '__doc__', '__members__', '__module__'): From bd30c46ee6784975a8c3fce271218945e143c2de Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 29 Oct 2021 20:26:00 +0100 Subject: [PATCH 04/12] Update enum.py --- Lib/enum.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/enum.py b/Lib/enum.py index 7089b8adaa7f3e..972dbe7fdc824c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -4,6 +4,7 @@ from functools import reduce from builtins import property as _bltin_property, bin as _bltin_bin + __all__ = [ 'EnumType', 'EnumMeta', 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', From 782f3c7f337e9fc6a53d7254d60643a0028ea20d Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Fri, 29 Oct 2021 21:02:30 +0100 Subject: [PATCH 05/12] Further simplify diff --- Lib/enum.py | 1 + Lib/test/test_enum.py | 32 ++++++++++++-------------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 7089b8adaa7f3e..972dbe7fdc824c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -4,6 +4,7 @@ from functools import reduce from builtins import property as _bltin_property, bin as _bltin_bin + __all__ = [ 'EnumType', 'EnumMeta', 'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index a06000a79ed31e..8497171d5e7b39 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -176,10 +176,6 @@ class Season(Enum): WINTER = 4 self.Season = Season - class FloatEnum(float, Enum): - pass - self.FloatEnum = FloatEnum - class Konstants(float, Enum): E = 2.7182818 PI = 3.1415926 @@ -202,15 +198,17 @@ class Directional(str, Enum): self.Directional = Directional from datetime import date - class DateEnum(date, Enum): - pass - self.DateEnum = DateEnum - class Holiday(date, Enum): NEW_YEAR = 2013, 1, 1 IDES_OF_MARCH = 2013, 3, 15 self.Holiday = Holiday + class DateEnum(date, Enum): pass + self.DateEnum = DateEnum + + class FloatEnum(float, Enum): pass + self.FloatEnum = FloatEnum + class Wowser(Enum): this = 'that' these = 'those' @@ -236,18 +234,15 @@ def wowser(self): self.FloatWowser = FloatWowser class WowserNoMembers(Enum): - def wowser(self): - return ("Wowser! I'm %s!" % self.name) + def wowser(self): pass self.WowserNoMembers = WowserNoMembers class IntWowserNoMembers(IntEnum): - def wowser(self): - return ("Wowser! I'm %s!" % self.name) + def wowser(self): pass self.IntWowserNoMembers = IntWowserNoMembers class FloatWowserNoMembers(float, Enum): - def wowser(self): - return ("Wowser! I'm %s!" % self.name) + def wowser(self): pass self.FloatWowserNoMembers = FloatWowserNoMembers class EnumWithInit(Enum): @@ -256,8 +251,7 @@ def __init__(self, greeting, farewell): self.farewell = farewell ENGLISH = 'hello', 'goodbye' GERMAN = 'Guten Morgen', 'Auf Wiedersehen' - def some_method(self): - pass + def some_method(self): pass self.EnumWithInit = EnumWithInit # see issue22506 @@ -4392,11 +4386,10 @@ 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_enum_dir = set(dir(IntEnum)) # 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', '__') - and name not in int_enum_dir], + and name not in dir(IntEnum)], [], msg='Names other than CONVERT_TEST_* found.') @unittest.skipUnless(python_version == (3, 8), @@ -4445,11 +4438,10 @@ 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_enum_dir = set(dir(StrEnum)) # 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', '__') - and name not in str_enum_dir], + and name not in dir(StrEnum)], [], msg='Names other than CONVERT_STR_* found.') def test_convert_repr_and_str(self): From 6dc0ec057418e7286a9d51cb239c9fabbdc2df35 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 30 Oct 2021 14:45:09 +0100 Subject: [PATCH 06/12] Lookup via __dict__ for non-EnumType classes --- Lib/enum.py | 18 ++++++++++-------- Lib/test/test_enum.py | 13 ++++++++++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 972dbe7fdc824c..9d15577fda5aa1 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -638,16 +638,15 @@ def __dir__(self): # Start off with the desired result for dir(Enum) cls_dir = {'__class__', '__doc__', '__members__', '__module__'} add_to_dir = cls_dir.add - - # 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() is_from_this_module = lambda cls: any(cls is thing for thing in this_module) first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) + # We want these added to __dir__ + # if and only if they have been user-overridden + enum_dunders = set(filter(_is_dunder, Enum.__dict__)) + # special-case __new__ if self.__new__ is not first_enum_base.__new__: add_to_dir('__new__') @@ -657,9 +656,12 @@ def __dir__(self): if cls is object or is_from_this_module(cls): continue - # Avoid dir() if EnumType is the metaclass (infinite recursion otherwise) - # Otherwise, go according to dir() - cls_lookup = cls.__dict__ if isinstance(cls, EnumType) else dir(cls) + cls_lookup = cls.__dict__ + + # If not an instance of EnumType, + # ensure all attributes excluded from that class's `dir()` are ignored. + if not isinstance(cls, EnumType): + cls_lookup = set(cls_lookup).intersection(dir(cls)) for attr_name in cls_lookup: # Exclude all sunders from dir(); __new__ is special-cased diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 8497171d5e7b39..192ee11a96c543 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -438,20 +438,27 @@ def test_mixin_dirs(self): for enum_cls, mixin_cls in enums_for_test: with self.subTest(enum_cls=enum_cls): cls_dir = dir(enum_cls) - mixin_dir = dir(mixin_cls) cls_dict = enum_cls.__dict__ + mixin_attrs = [ + x for x in dir(mixin_cls) + if ( + getattr(mixin_cls, x) is not getattr(object, x, object()) + and x not in {'__init_subclass__', '__subclasshook__'} + ) + ] + first_enum_base = next( base for base in enum_cls.__mro__ if is_from_enum_module(base) ) - for attr in mixin_dir: + for attr in mixin_attrs: with self.subTest(attr=attr): if enum._is_sunder(attr): # Unlikely, but no harm in testing self.assertNotIn(attr, cls_dir) - elif attr in ('__class__', '__doc__', '__members__', '__module__'): + elif attr in {'__class__', '__doc__', '__members__', '__module__'}: self.assertIn(attr, cls_dir) elif is_enum_dunder(attr): if is_from_enum_module(enum_cls): From f5f80ad908a8ab893d984a6dc656ee401e77cdf3 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 30 Oct 2021 14:59:10 +0100 Subject: [PATCH 07/12] Small tweaks --- Lib/enum.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 9d15577fda5aa1..71d9a4ce0b4057 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -642,6 +642,8 @@ def __dir__(self): this_module = globals().values() is_from_this_module = lambda cls: any(cls is thing for thing in this_module) first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) + ignored = set() + add_to_ignored = ignored.add # We want these added to __dir__ # if and only if they have been user-overridden @@ -664,12 +666,17 @@ def __dir__(self): cls_lookup = set(cls_lookup).intersection(dir(cls)) for attr_name in cls_lookup: + # Already seen it? Carry on + if attr_name in cls_dir or attr_name in ignored: + continue # Exclude all sunders from dir(); __new__ is special-cased - if attr_name == '__new__' or _is_sunder(attr_name): + elif attr_name == '__new__' or _is_sunder(attr_name): continue elif attr_name not in enum_dunders: add_to_dir(attr_name) - elif getattr(self, attr_name) is not getattr(first_enum_base, attr_name, object()): + elif getattr(self, attr_name) is getattr(first_enum_base, attr_name, object()): + add_to_ignored(attr_name) + else: add_to_dir(attr_name) # sort the output before returning it, so that the result is deterministic. From 6d182a09fa6c3e3fbbdc1930b9684d0c3d6af024 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sat, 30 Oct 2021 22:39:23 +0100 Subject: [PATCH 08/12] More tweaks --- Lib/enum.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 71d9a4ce0b4057..905e8ff6828e66 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -642,7 +642,8 @@ def __dir__(self): this_module = globals().values() is_from_this_module = lambda cls: any(cls is thing for thing in this_module) first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) - ignored = set() + # special-case __new__ + ignored = {'__new__'} add_to_ignored = ignored.add # We want these added to __dir__ @@ -661,7 +662,7 @@ def __dir__(self): cls_lookup = cls.__dict__ # If not an instance of EnumType, - # ensure all attributes excluded from that class's `dir()` are ignored. + # ensure all attributes excluded from that class's `dir()` are ignored here. if not isinstance(cls, EnumType): cls_lookup = set(cls_lookup).intersection(dir(cls)) @@ -669,13 +670,17 @@ def __dir__(self): # Already seen it? Carry on if attr_name in cls_dir or attr_name in ignored: continue - # Exclude all sunders from dir(); __new__ is special-cased - elif attr_name == '__new__' or _is_sunder(attr_name): - continue + # Exclude all sunders from dir() + elif _is_sunder(attr_name): + add_to_ignored(attr_name) + # Not an "enum dunder"? Add it to dir() output. elif attr_name not in enum_dunders: add_to_dir(attr_name) + # Is an "enum dunder", and is defined by a class from enum.py? Ignore it. elif getattr(self, attr_name) is getattr(first_enum_base, attr_name, object()): add_to_ignored(attr_name) + # Is an "enum dunder", and is either user-defined or defined by a mixin class? + # Add it to dir() output. else: add_to_dir(attr_name) From 6b22112609db48a9440df418642cf18c69e7c580 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 31 Oct 2021 17:55:06 +0000 Subject: [PATCH 09/12] Improve tests w.r.t. classmethods --- Lib/test/test_enum.py | 47 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 192ee11a96c543..0c9e063d6cca89 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -215,6 +215,10 @@ class Wowser(Enum): def wowser(self): """Wowser docstring""" return ("Wowser! I'm %s!" % self.name) + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass self.Wowser = Wowser class IntWowser(IntEnum): @@ -223,6 +227,10 @@ class IntWowser(IntEnum): def wowser(self): """Wowser docstring""" return ("Wowser! I'm %s!" % self.name) + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass self.IntWowser = IntWowser class FloatWowser(float, Enum): @@ -231,18 +239,36 @@ class FloatWowser(float, Enum): def wowser(self): """Wowser docstring""" return ("Wowser! I'm %s!" % self.name) + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass self.FloatWowser = FloatWowser class WowserNoMembers(Enum): def wowser(self): pass + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass + class SubclassOfWowserNoMembers(WowserNoMembers): pass self.WowserNoMembers = WowserNoMembers + self.SubclassOfWowserNoMembers = SubclassOfWowserNoMembers class IntWowserNoMembers(IntEnum): def wowser(self): pass + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass self.IntWowserNoMembers = IntWowserNoMembers class FloatWowserNoMembers(float, Enum): def wowser(self): pass + @classmethod + def classmethod_wowser(cls): pass + @staticmethod + def staticmethod_wowser(): pass self.FloatWowserNoMembers = FloatWowserNoMembers class EnumWithInit(Enum): @@ -370,6 +396,7 @@ def test_dir_for_enums_with_added_behaviour(self): self.IntWowser, self.FloatWowser, self.WowserNoMembers, + self.SubclassOfWowserNoMembers, self.IntWowserNoMembers, self.FloatWowserNoMembers ) @@ -377,7 +404,12 @@ def test_dir_for_enums_with_added_behaviour(self): for cls in enums_for_test: with self.subTest(cls=cls): self.assertIn('wowser', dir(cls)) - self.assertTrue(all('wowser' in dir(member) for member in cls)) + self.assertIn('classmethod_wowser', dir(cls)) + self.assertIn('staticmethod_wowser', dir(cls)) + self.assertTrue(all( + all(attr in dir(member) for attr in ('wowser', 'classmethod_wowser', 'staticmethod_wowser')) + for member in cls + )) def test_help_output_on_enum_members(self): added_behaviour_enums = ( @@ -434,6 +466,12 @@ def test_mixin_dirs(self): is_from_enum_module = lambda cls: cls.__name__ in enum_module_names is_enum_dunder = lambda attr: enum._is_dunder(attr) and attr in enum_dict + def attr_is_inherited_from_object(cls, attr_name): + for base in cls.__mro__: + if attr_name in base.__dict__: + return base is object + return False + # General tests for enum_cls, mixin_cls in enums_for_test: with self.subTest(enum_cls=enum_cls): @@ -442,10 +480,7 @@ def test_mixin_dirs(self): mixin_attrs = [ x for x in dir(mixin_cls) - if ( - getattr(mixin_cls, x) is not getattr(object, x, object()) - and x not in {'__init_subclass__', '__subclasshook__'} - ) + if not attr_is_inherited_from_object(cls=mixin_cls, attr_name=x) ] first_enum_base = next( @@ -476,6 +511,8 @@ def test_mixin_dirs(self): self.assertIn('__rfloordiv__', int_enum_dir) self.assertNotIn('__format__', int_enum_dir) self.assertNotIn('__hash__', int_enum_dir) + self.assertNotIn('__init_subclass__', int_enum_dir) + self.assertNotIn('__subclasshook__', int_enum_dir) class OverridesFormatOutsideEnumModule(Enum): def __format__(self, *args, **kwargs): From baa84775938fe5507d10468f8ecfdb6d5f3e5bcb Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Sun, 31 Oct 2021 18:25:57 +0000 Subject: [PATCH 10/12] More test tweaks --- Lib/test/test_enum.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index 0c9e063d6cca89..eecb9fd4835c40 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -411,6 +411,11 @@ def test_dir_for_enums_with_added_behaviour(self): for member in cls )) + self.assertEqual(dir(self.WowserNoMembers), dir(self.SubclassOfWowserNoMembers)) + # Check classmethods are present + self.assertIn('from_bytes', dir(self.IntWowser)) + self.assertIn('from_bytes', dir(self.IntWowserNoMembers)) + def test_help_output_on_enum_members(self): added_behaviour_enums = ( self.Wowser, From f307b22b9647f8182984b450a8729c01d5c28052 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 2 Nov 2021 00:00:08 +0000 Subject: [PATCH 11/12] Another small tweak --- Lib/enum.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/enum.py b/Lib/enum.py index 905e8ff6828e66..23fcf2412f1ead 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -642,13 +642,14 @@ def __dir__(self): this_module = globals().values() is_from_this_module = lambda cls: any(cls is thing for thing in this_module) first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) + enum_dict = Enum.__dict__ # special-case __new__ - ignored = {'__new__'} + ignored = {'__new__', *filter(_is_sunder, enum_dict)} add_to_ignored = ignored.add # We want these added to __dir__ # if and only if they have been user-overridden - enum_dunders = set(filter(_is_dunder, Enum.__dict__)) + enum_dunders = set(filter(_is_dunder, enum_dict)) # special-case __new__ if self.__new__ is not first_enum_base.__new__: @@ -670,7 +671,8 @@ def __dir__(self): # Already seen it? Carry on if attr_name in cls_dir or attr_name in ignored: continue - # Exclude all sunders from dir() + # Sunders defined in Enum.__dict__ are already in `ignored`, + # But sunders defined in a subclass won't be (we want all sunders excluded). elif _is_sunder(attr_name): add_to_ignored(attr_name) # Not an "enum dunder"? Add it to dir() output. From ce022300c648276286f7d786a9c5352829a95587 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 2 Dec 2021 13:10:14 +0000 Subject: [PATCH 12/12] Only use a single sentinel --- Lib/enum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/enum.py b/Lib/enum.py index 23fcf2412f1ead..8efc38c3d78dbc 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -643,6 +643,7 @@ def __dir__(self): is_from_this_module = lambda cls: any(cls is thing for thing in this_module) first_enum_base = next(cls for cls in mro if is_from_this_module(cls)) enum_dict = Enum.__dict__ + sentinel = object() # special-case __new__ ignored = {'__new__', *filter(_is_sunder, enum_dict)} add_to_ignored = ignored.add @@ -679,7 +680,7 @@ def __dir__(self): elif attr_name not in enum_dunders: add_to_dir(attr_name) # Is an "enum dunder", and is defined by a class from enum.py? Ignore it. - elif getattr(self, attr_name) is getattr(first_enum_base, attr_name, object()): + elif getattr(self, attr_name) is getattr(first_enum_base, attr_name, sentinel): add_to_ignored(attr_name) # Is an "enum dunder", and is either user-defined or defined by a mixin class? # Add it to dir() output.