diff options
author | Alex Waygood <Alex.Waygood@Gmail.com> | 2021-12-02 16:49:52 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-12-02 16:49:52 (GMT) |
commit | b2afdc95cc8f4e9228148730949a43cef0323f15 (patch) | |
tree | 00f99a2e959b803ee11434f6866215ac63dc90e6 /Lib/enum.py | |
parent | cb8f491f46e262549f6c447b31625cab7c20a60a (diff) | |
download | cpython-b2afdc95cc8f4e9228148730949a43cef0323f15.zip cpython-b2afdc95cc8f4e9228148730949a43cef0323f15.tar.gz cpython-b2afdc95cc8f4e9228148730949a43cef0323f15.tar.bz2 |
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.
Diffstat (limited to 'Lib/enum.py')
-rw-r--r-- | Lib/enum.py | 69 |
1 files changed, 58 insertions, 11 deletions
diff --git a/Lib/enum.py b/Lib/enum.py index 461d276..8efc38c 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -635,10 +635,60 @@ class EnumType(type): 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): """ @@ -985,13 +1035,10 @@ class Enum(metaclass=EnumType): """ 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): """ |