Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-45535: Improve output of Enum dir() #29316

Merged
merged 13 commits into from
Dec 2, 2021
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
66 changes: 55 additions & 11 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,10 +635,57 @@ 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))
# special-case __new__
ignored = {'__new__'}
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
# 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)

# 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 +1032,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