Skip to content

Commit b2afdc9

Browse files
authored
bpo-45535: Improve output of Enum dir() (GH-29316)
Modify 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.
1 parent cb8f491 commit b2afdc9

File tree

5 files changed

+386
-53
lines changed

5 files changed

+386
-53
lines changed

Doc/howto/enum.rst

+4-3
Original file line numberDiff line numberDiff line change
@@ -997,11 +997,12 @@ Plain :class:`Enum` classes always evaluate as :data:`True`.
997997
"""""""""""""""""""""""""""""
998998

999999
If you give your enum subclass extra methods, like the `Planet`_
1000-
class below, those methods will show up in a :func:`dir` of the member,
1001-
but not of the class::
1000+
class below, those methods will show up in a :func:`dir` of the member and the
1001+
class. Attributes defined in an :func:`__init__` method will only show up in a
1002+
:func:`dir` of the member::
10021003

10031004
>>> dir(Planet)
1004-
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
1005+
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__init__', '__members__', '__module__', 'surface_gravity']
10051006
>>> dir(Planet.EARTH)
10061007
['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value']
10071008

Doc/library/enum.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ Data Types
162162
.. method:: EnumType.__dir__(cls)
163163

164164
Returns ``['__class__', '__doc__', '__members__', '__module__']`` and the
165-
names of the members in *cls*::
165+
names of the members in ``cls``. User-defined methods and methods from
166+
mixin classes will also be included::
166167

167168
>>> dir(Color)
168169
['BLUE', 'GREEN', 'RED', '__class__', '__doc__', '__members__', '__module__']
@@ -260,7 +261,7 @@ Data Types
260261
.. method:: Enum.__dir__(self)
261262

262263
Returns ``['__class__', '__doc__', '__module__', 'name', 'value']`` and
263-
any public methods defined on *self.__class__*::
264+
any public methods defined on ``self.__class__`` or a mixin class::
264265

265266
>>> from datetime import date
266267
>>> class Weekday(Enum):

Lib/enum.py

+58-11
Original file line numberDiff line numberDiff line change
@@ -635,10 +635,60 @@ def __delattr__(cls, attr):
635635
super().__delattr__(attr)
636636

637637
def __dir__(self):
638-
return (
639-
['__class__', '__doc__', '__members__', '__module__']
640-
+ self._member_names_
641-
)
638+
# Start off with the desired result for dir(Enum)
639+
cls_dir = {'__class__', '__doc__', '__members__', '__module__'}
640+
add_to_dir = cls_dir.add
641+
mro = self.__mro__
642+
this_module = globals().values()
643+
is_from_this_module = lambda cls: any(cls is thing for thing in this_module)
644+
first_enum_base = next(cls for cls in mro if is_from_this_module(cls))
645+
enum_dict = Enum.__dict__
646+
sentinel = object()
647+
# special-case __new__
648+
ignored = {'__new__', *filter(_is_sunder, enum_dict)}
649+
add_to_ignored = ignored.add
650+
651+
# We want these added to __dir__
652+
# if and only if they have been user-overridden
653+
enum_dunders = set(filter(_is_dunder, enum_dict))
654+
655+
# special-case __new__
656+
if self.__new__ is not first_enum_base.__new__:
657+
add_to_dir('__new__')
658+
659+
for cls in mro:
660+
# Ignore any classes defined in this module
661+
if cls is object or is_from_this_module(cls):
662+
continue
663+
664+
cls_lookup = cls.__dict__
665+
666+
# If not an instance of EnumType,
667+
# ensure all attributes excluded from that class's `dir()` are ignored here.
668+
if not isinstance(cls, EnumType):
669+
cls_lookup = set(cls_lookup).intersection(dir(cls))
670+
671+
for attr_name in cls_lookup:
672+
# Already seen it? Carry on
673+
if attr_name in cls_dir or attr_name in ignored:
674+
continue
675+
# Sunders defined in Enum.__dict__ are already in `ignored`,
676+
# But sunders defined in a subclass won't be (we want all sunders excluded).
677+
elif _is_sunder(attr_name):
678+
add_to_ignored(attr_name)
679+
# Not an "enum dunder"? Add it to dir() output.
680+
elif attr_name not in enum_dunders:
681+
add_to_dir(attr_name)
682+
# Is an "enum dunder", and is defined by a class from enum.py? Ignore it.
683+
elif getattr(self, attr_name) is getattr(first_enum_base, attr_name, sentinel):
684+
add_to_ignored(attr_name)
685+
# Is an "enum dunder", and is either user-defined or defined by a mixin class?
686+
# Add it to dir() output.
687+
else:
688+
add_to_dir(attr_name)
689+
690+
# sort the output before returning it, so that the result is deterministic.
691+
return sorted(cls_dir)
642692

643693
def __getattr__(cls, name):
644694
"""
@@ -985,13 +1035,10 @@ def __dir__(self):
9851035
"""
9861036
Returns all members and all public methods
9871037
"""
988-
added_behavior = [
989-
m
990-
for cls in self.__class__.mro()
991-
for m in cls.__dict__
992-
if m[0] != '_' and m not in self._member_map_
993-
] + [m for m in self.__dict__ if m[0] != '_']
994-
return (['__class__', '__doc__', '__module__'] + added_behavior)
1038+
cls = type(self)
1039+
to_exclude = {'__members__', '__init__', '__new__', *cls._member_names_}
1040+
filtered_self_dict = (name for name in self.__dict__ if not name.startswith('_'))
1041+
return sorted({'name', 'value', *dir(cls), *filtered_self_dict} - to_exclude)
9951042

9961043
def __format__(self, format_spec):
9971044
"""

0 commit comments

Comments
 (0)