Skip to content
Merged
7 changes: 4 additions & 3 deletions Doc/howto/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does __init__ need to be here? It will never be called after the enum class is created.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surface_gravity is here so completion works?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having __init__ show up on dir(Planet) improves the output of help(Planet.EARTH). Without __init__ being present on dir(Planet), there is no reference in the output from help(Planet.EARTH) to the fact that the member has two named attributes, mass and radius. With __init__ being present on dir(Planet), however, the signature of the __init__ function shows up in the output of help(Planet.EARTH), so we can clearly see that the member has two named attributes, mass and radius.

>>> dir(Planet.EARTH)
['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value']

Expand Down
5 changes: 3 additions & 2 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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__']
Expand Down Expand Up @@ -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):
Expand Down
69 changes: 58 additions & 11 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,10 +635,60 @@ 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
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))
enum_dict = Enum.__dict__
sentinel = object()
# special-case __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))

# 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

cls_lookup = cls.__dict__

# If not an instance of EnumType,
# 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))

for attr_name in cls_lookup:
# Already seen it? Carry on
if attr_name in cls_dir or attr_name in ignored:
continue
# 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.
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, 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.
else:
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):
"""
Expand Down Expand Up @@ -985,13 +1035,10 @@ 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)
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', *dir(cls), *filtered_self_dict} - to_exclude)

def __format__(self, format_spec):
"""
Expand Down
Loading