Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
wip
  • Loading branch information
ethanfurman committed Jan 14, 2022
commit 06f0dc8d3993f38b26a866353ac421418bd122c9
34 changes: 18 additions & 16 deletions Doc/howto/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -816,25 +816,28 @@ Some rules:
1. When subclassing :class:`Enum`, mix-in types must appear before
:class:`Enum` itself in the sequence of bases, as in the :class:`IntEnum`
example above.
2. Mix-in types must be subclassable. For example,
:class:`bool` and :class:`range` are not subclassable
and will throw an error during Enum creation if used as the mix-in type.
2. Mix-in types must be subclassable. For example, :class:`bool` and
:class:`range` are not subclassable and will throw an error during Enum
creation if used as the mix-in type.
3. While :class:`Enum` can have members of any type, once you mix in an
additional type, all the members must have values of that type, e.g.
:class:`int` above. This restriction does not apply to mix-ins which only
add methods and don't specify another type.
4. When another data type is mixed in, the :attr:`value` attribute is *not the
same* as the enum member itself, although it is equivalent and will compare
equal.
5. %-style formatting: `%s` and `%r` call the :class:`Enum` class's
5. %-style formatting: ``%s`` and ``%r`` call the :class:`Enum` class's
:meth:`__str__` and :meth:`__repr__` respectively; other codes (such as
`%i` or `%h` for IntEnum) treat the enum member as its mixed-in type.
``%i`` or ``%h`` for IntEnum) treat the enum member as its mixed-in type.
6. :ref:`Formatted string literals <f-strings>`, :meth:`str.format`,
and :func:`format` will use the mixed-in type's :meth:`__format__`
unless :meth:`__str__` or :meth:`__format__` is overridden in the subclass,
in which case the overridden methods or :class:`Enum` methods will be used.
Use the !s and !r format codes to force usage of the :class:`Enum` class's
:meth:`__str__` and :meth:`__repr__` methods.
and :func:`format` will use the enum's :meth:`__str__` method.

.. note::

Because :class:`IntEnum`, :class:`IntFlag`, and :class:`StrEnum` are
designed to be drop-in replacements for existing constants, their
:meth:`__str__` method has been reset to their data types
:meth:`__str__` method.

When to use :meth:`__new__` vs. :meth:`__init__`
------------------------------------------------
Expand Down Expand Up @@ -998,13 +1001,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 and the
class. Attributes defined in an :func:`__init__` method will only show up in a
:func:`dir` of the member::
class below, those methods will show up in a :func:`dir` of the member,
but not of the class::

>>> dir(Planet)
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__init__', '__members__', '__module__', 'surface_gravity']
>>> dir(Planet.EARTH)
>>> dir(Planet) # doctest: +SKIP
['EARTH', 'JUPITER', 'MARS', 'MERCURY', 'NEPTUNE', 'SATURN', 'URANUS', 'VENUS', '__class__', '__doc__', '__members__', '__module__']
>>> dir(Planet.EARTH) # doctest: +SKIP
['__class__', '__doc__', '__module__', 'mass', 'name', 'radius', 'surface_gravity', 'value']


Expand Down
7 changes: 3 additions & 4 deletions Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,7 @@ Data Types
.. method:: EnumType.__dir__(cls)

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

>>> dir(Color)
['BLUE', 'GREEN', 'RED', '__class__', '__contains__', '__doc__', '__getitem__', '__iter__', '__len__', '__members__', '__module__', '__name__', '__qualname__']
Expand All @@ -172,7 +171,7 @@ Data Types

.. method:: EnumType.__getitem__(cls, name)

Returns the Enum member in *cls* matching *name*, or raises a :exc:`KeyError`::
Returns the Enum member in *cls* matching *name*, or raises an :exc:`KeyError`::

>>> Color['BLUE']
<Color.BLUE: 3>
Expand Down Expand Up @@ -256,7 +255,7 @@ Data Types
.. method:: Enum.__dir__(self)

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

>>> from datetime import date
>>> class Weekday(Enum):
Expand Down
132 changes: 49 additions & 83 deletions Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,57 +650,30 @@ def __delattr__(cls, attr):
raise AttributeError("%r cannot delete member %r." % (cls.__name__, attr))
super().__delattr__(attr)

def __dir__(self):
# 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))

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 __dir__(cls):
# TODO: check for custom __init__, __new__, __format__, __repr__, __str__, __init_subclass__
# on object-based enums
if cls._member_type_ is object:
interesting = set(cls._member_names_)
if cls._new_member_ is not object.__new__:
interesting.add('__new__')
if cls.__init_subclass__ is not object.__init_subclass__:
interesting.add('__init_subclass__')
for method in ('__init__', '__format__', '__repr__', '__str__'):
if getattr(cls, method) not in (getattr(Enum, method), getattr(Flag, method)):
interesting.add(method)
return sorted(set([
'__class__', '__contains__', '__doc__', '__getitem__',
'__iter__', '__len__', '__members__', '__module__',
'__name__', '__qualname__',
]) | interesting
)
else:
# return whatever mixed-in data type has
return sorted(set(
dir(cls._member_type_)
+ cls._member_names_
))

def __getattr__(cls, name):
"""
Expand Down Expand Up @@ -1072,38 +1045,31 @@ def __dir__(self):
"""
Returns all members and all public methods
"""
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):
# """
# Returns format using actual value type unless __str__ has been overridden.
# """
# # mixed-in Enums should use the mixed-in type's __format__, otherwise
# # we can get strange results with the Enum name showing up instead of
# # the value
# #
# # pure Enum branch, or branch with __str__ explicitly overridden
# str_overridden = type(self).__str__ not in (Enum.__str__, IntEnum.__str__, Flag.__str__)
# if self._member_type_ is object or str_overridden:
# cls = str
# val = str(self)
# # mix-in branch
# else:
# if not format_spec or format_spec in ('{}','{:}'):
# import warnings
# warnings.warn(
# "in 3.12 format() will use the enum member, not the enum member's value;\n"
# "use a format specifier, such as :d for an integer-based Enum, to maintain "
# "the current display",
# DeprecationWarning,
# stacklevel=2,
# )
# cls = self._member_type_
# val = self._value_
# return cls.__format__(val, format_spec)
if self.__class__._member_type_ is object:
interesting = set(['__class__', '__doc__', '__eq__', '__hash__', '__module__', 'name', 'value'])
else:
interesting = set(object.__dir__(self))
for name in getattr(self, '__dict__', []):
if name[0] != '_':
interesting.add(name)
for cls in self.__class__.mro():
for name, obj in cls.__dict__.items():
if name[0] == '_':
continue
if isinstance(obj, property):
# that's an enum.property
if obj.fget is not None or name not in self._member_map_:
interesting.add(name)
else:
# in case it was added by `dir(self)`
interesting.discard(name)
else:
interesting.add(name)
names = sorted(
set(['__class__', '__doc__', '__eq__', '__hash__', '__module__'])
| interesting
)
return names

def __format__(self, format_spec):
return str.__format__(str(self), format_spec)
Expand Down
Loading