[Enum] reformat and add doc strings (GH-23705)

This commit is contained in:
Ethan Furman 2020-12-08 12:26:56 -08:00 committed by GitHub
parent 37440eef7f
commit 6d3dfee271
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 135 additions and 54 deletions

View File

@ -10,31 +10,41 @@ __all__ = [
def _is_descriptor(obj):
"""Returns True if obj is a descriptor, False otherwise."""
"""
Returns True if obj is a descriptor, False otherwise.
"""
return (
hasattr(obj, '__get__') or
hasattr(obj, '__set__') or
hasattr(obj, '__delete__'))
hasattr(obj, '__delete__')
)
def _is_dunder(name):
"""Returns True if a __dunder__ name, False otherwise."""
return (len(name) > 4 and
"""
Returns True if a __dunder__ name, False otherwise.
"""
return (
len(name) > 4 and
name[:2] == name[-2:] == '__' and
name[2] != '_' and
name[-3] != '_')
name[-3] != '_'
)
def _is_sunder(name):
"""Returns True if a _sunder_ name, False otherwise."""
return (len(name) > 2 and
"""
Returns True if a _sunder_ name, False otherwise.
"""
return (
len(name) > 2 and
name[0] == name[-1] == '_' and
name[1:2] != '_' and
name[-2:-1] != '_')
name[-2:-1] != '_'
)
def _make_class_unpicklable(cls):
"""Make the given class un-picklable."""
"""
Make the given class un-picklable.
"""
def _break_on_call_reduce(self, proto):
raise TypeError('%r cannot be pickled' % self)
cls.__reduce_ex__ = _break_on_call_reduce
@ -49,11 +59,11 @@ class auto:
class _EnumDict(dict):
"""Track enum member order and ensure member names are not reused.
"""
Track enum member order and ensure member names are not reused.
EnumMeta will use the names found in self._member_names as the
enumeration member names.
"""
def __init__(self):
super().__init__()
@ -63,21 +73,23 @@ class _EnumDict(dict):
self._auto_called = False
def __setitem__(self, key, value):
"""Changes anything not dundered or not a descriptor.
"""
Changes anything not dundered or not a descriptor.
If an enum member name is used twice, an error is raised; duplicate
values are not checked for.
Single underscore (sunder) names are reserved.
"""
if _is_sunder(key):
if key not in (
'_order_', '_create_pseudo_member_',
'_generate_next_value_', '_missing_', '_ignore_',
):
raise ValueError(f'_sunder_ names, such as "{key}", are '
'reserved for future Enum use')
raise ValueError(
'_sunder_ names, such as %r, are reserved for future Enum use'
% (key, )
)
if key == '_generate_next_value_':
# check if members already defined as auto()
if self._auto_called:
@ -91,7 +103,10 @@ class _EnumDict(dict):
self._ignore = value
already = set(value) & set(self._member_names)
if already:
raise ValueError('_ignore_ cannot specify already set names: %r' % (already, ))
raise ValueError(
'_ignore_ cannot specify already set names: %r'
% (already, )
)
elif _is_dunder(key):
if key == '__order__':
key = '_order_'
@ -106,7 +121,12 @@ class _EnumDict(dict):
raise TypeError('%r already defined as: %r' % (key, self[key]))
if isinstance(value, auto):
if value.value == _auto_null:
value.value = self._generate_next_value(key, 1, len(self._member_names), self._last_values[:])
value.value = self._generate_next_value(
key,
1,
len(self._member_names),
self._last_values[:],
)
self._auto_called = True
value = value.value
self._member_names.append(key)
@ -119,9 +139,10 @@ class _EnumDict(dict):
# This is also why there are checks in EnumMeta like `if Enum is not None`
Enum = None
class EnumMeta(type):
"""Metaclass for Enum"""
"""
Metaclass for Enum
"""
@classmethod
def __prepare__(metacls, cls, bases):
# check that previous enum members do not exist
@ -131,7 +152,9 @@ class EnumMeta(type):
# inherit previous flags and _generate_next_value_ function
member_type, first_enum = metacls._get_mixins_(cls, bases)
if first_enum is not None:
enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None)
enum_dict['_generate_next_value_'] = getattr(
first_enum, '_generate_next_value_', None,
)
return enum_dict
def __new__(metacls, cls, bases, classdict):
@ -177,9 +200,11 @@ class EnumMeta(type):
# save DynamicClassAttribute attributes from super classes so we know
# if we can take the shortcut of storing members in the class dict
dynamic_attributes = {k for c in enum_class.mro()
dynamic_attributes = {
k for c in enum_class.mro()
for k, v in c.__dict__.items()
if isinstance(v, DynamicClassAttribute)}
if isinstance(v, DynamicClassAttribute)
}
# Reverse value->name map for hashable values.
enum_class._value2member_map_ = {}
@ -289,7 +314,8 @@ class EnumMeta(type):
return True
def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
"""Either returns an existing member, or creates a new enum class.
"""
Either returns an existing member, or creates a new enum class.
This method is used both when an enum class is given a value to match
to an enumeration member (i.e. Color(3)) and for the functional API
@ -311,12 +337,18 @@ class EnumMeta(type):
not correct, unpickling will fail in some circumstances.
`type`, if set, will be mixed in as the first base class.
"""
if names is None: # simple value lookup
return cls.__new__(cls, value)
# otherwise, functional API: we're creating a new Enum type
return cls._create_(value, names, module=module, qualname=qualname, type=type, start=start)
return cls._create_(
value,
names,
module=module,
qualname=qualname,
type=type,
start=start,
)
def __contains__(cls, member):
if not isinstance(member, Enum):
@ -329,22 +361,23 @@ class EnumMeta(type):
# nicer error message when someone tries to delete an attribute
# (see issue19025).
if attr in cls._member_map_:
raise AttributeError(
"%s: cannot delete Enum member." % cls.__name__)
raise AttributeError("%s: cannot delete Enum member %r." % (cls.__name__, attr))
super().__delattr__(attr)
def __dir__(self):
return (['__class__', '__doc__', '__members__', '__module__'] +
self._member_names_)
return (
['__class__', '__doc__', '__members__', '__module__']
+ self._member_names_
)
def __getattr__(cls, name):
"""Return the enum member matching `name`
"""
Return the enum member matching `name`
We use __getattr__ instead of descriptors or inserting into the enum
class' __dict__ in order to support `name` and `value` being both
properties for enum members (which live in the class' __dict__) and
enum members themselves.
"""
if _is_dunder(name):
raise AttributeError(name)
@ -357,6 +390,9 @@ class EnumMeta(type):
return cls._member_map_[name]
def __iter__(cls):
"""
Returns members in definition order.
"""
return (cls._member_map_[name] for name in cls._member_names_)
def __len__(cls):
@ -364,11 +400,11 @@ class EnumMeta(type):
@property
def __members__(cls):
"""Returns a mapping of member name->value.
"""
Returns a mapping of member name->value.
This mapping lists all enum members, including aliases. Note that this
is a read-only view of the internal mapping.
"""
return MappingProxyType(cls._member_map_)
@ -376,15 +412,18 @@ class EnumMeta(type):
return "<enum %r>" % cls.__name__
def __reversed__(cls):
"""
Returns members in reverse definition order.
"""
return (cls._member_map_[name] for name in reversed(cls._member_names_))
def __setattr__(cls, name, value):
"""Block attempts to reassign Enum members.
"""
Block attempts to reassign Enum members.
A simple assignment to the class namespace only changes one of the
several possible ways to get an Enum member from the Enum class,
resulting in an inconsistent Enumeration.
"""
member_map = cls.__dict__.get('_member_map_', {})
if name in member_map:
@ -392,7 +431,8 @@ class EnumMeta(type):
super().__setattr__(name, value)
def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, start=1):
"""Convenience method to create a new Enum class.
"""
Convenience method to create a new Enum class.
`names` can be:
@ -401,7 +441,6 @@ class EnumMeta(type):
* An iterable of member names. Values are incremented by 1 from `start`.
* An iterable of (member name, value) pairs.
* A mapping of member name -> value pairs.
"""
metacls = cls.__class__
bases = (cls, ) if type is None else (type, cls)
@ -482,15 +521,18 @@ class EnumMeta(type):
for chain in bases:
for base in chain.__mro__:
if issubclass(base, Enum) and base._member_names_:
raise TypeError("%s: cannot extend enumeration %r" % (class_name, base.__name__))
raise TypeError(
"%s: cannot extend enumeration %r"
% (class_name, base.__name__)
)
@staticmethod
def _get_mixins_(class_name, bases):
"""Returns the type for creating enum members, and the first inherited
"""
Returns the type for creating enum members, and the first inherited
enum class.
bases: the tuple of bases that was given to __new__
"""
if not bases:
return object, Enum
@ -533,12 +575,12 @@ class EnumMeta(type):
@staticmethod
def _find_new_(classdict, member_type, first_enum):
"""Returns the __new__ to be used for creating the enum members.
"""
Returns the __new__ to be used for creating the enum members.
classdict: the class dictionary given to __new__
member_type: the data type whose __new__ will be used by default
first_enum: enumeration to check for an overriding __new__
"""
# now find the correct __new__, checking to see of one was defined
# by the user; also check earlier enum classes in case a __new__ was
@ -578,10 +620,10 @@ class EnumMeta(type):
class Enum(metaclass=EnumMeta):
"""Generic enumeration.
"""
Generic enumeration.
Derive from this class to define new enumerations.
"""
def __new__(cls, value):
# all enum instances are actually created during class construction
@ -624,6 +666,14 @@ class Enum(metaclass=EnumMeta):
raise exc
def _generate_next_value_(name, start, count, last_values):
"""
Generate the next value when not given.
name: the name of the member
start: the initial start value or None
count: the number of existing members
last_value: the last value assigned or None
"""
for last_value in reversed(last_values):
try:
return last_value + 1
@ -644,6 +694,9 @@ class Enum(metaclass=EnumMeta):
return "%s.%s" % (self.__class__.__name__, self._name_)
def __dir__(self):
"""
Returns all members and all public methods
"""
added_behavior = [
m
for cls in self.__class__.mro()
@ -653,6 +706,9 @@ class Enum(metaclass=EnumMeta):
return (['__class__', '__doc__', '__module__'] + added_behavior)
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
@ -730,7 +786,9 @@ def _reduce_ex_by_name(self, proto):
return self.name
class Flag(Enum):
"""Support for flags"""
"""
Support for flags
"""
def _generate_next_value_(name, start, count, last_values):
"""
@ -753,6 +811,9 @@ class Flag(Enum):
@classmethod
def _missing_(cls, value):
"""
Returns member (possibly creating it) if one can be found for value.
"""
original_value = value
if value < 0:
value = ~value
@ -782,6 +843,9 @@ class Flag(Enum):
return pseudo_member
def __contains__(self, other):
"""
Returns True if self has at least the same flags set as other.
"""
if not isinstance(other, self.__class__):
raise TypeError(
"unsupported operand type(s) for 'in': '%s' and '%s'" % (
@ -789,6 +853,9 @@ class Flag(Enum):
return other._value_ & self._value_ == other._value_
def __iter__(self):
"""
Returns flags in decreasing value order.
"""
members, extra_flags = _decompose(self.__class__, self.value)
return (m for m in members if m._value_ != 0)
@ -844,10 +911,15 @@ class Flag(Enum):
class IntFlag(int, Flag):
"""Support for integer-based Flags"""
"""
Support for integer-based Flags
"""
@classmethod
def _missing_(cls, value):
"""
Returns member (possibly creating it) if one can be found for value.
"""
if not isinstance(value, int):
raise ValueError("%r is not a valid %s" % (value, cls.__qualname__))
new_member = cls._create_pseudo_member_(value)
@ -855,6 +927,9 @@ class IntFlag(int, Flag):
@classmethod
def _create_pseudo_member_(cls, value):
"""
Create a composite member iff value contains only members.
"""
pseudo_member = cls._value2member_map_.get(value, None)
if pseudo_member is None:
need_to_create = [value]
@ -909,11 +984,15 @@ class IntFlag(int, Flag):
def _high_bit(value):
"""returns index of highest bit, or -1 if value is zero or negative"""
"""
returns index of highest bit, or -1 if value is zero or negative
"""
return value.bit_length() - 1
def unique(enumeration):
"""Class decorator for enumerations ensuring unique member values."""
"""
Class decorator for enumerations ensuring unique member values.
"""
duplicates = []
for name, member in enumeration.__members__.items():
if name != member.name:
@ -926,7 +1005,9 @@ def unique(enumeration):
return enumeration
def _decompose(flag, value):
"""Extract all members from the value."""
"""
Extract all members from the value.
"""
# _decompose is only called if the value is not named
not_covered = value
negative = value < 0

View File

@ -429,7 +429,7 @@ class TestEnum(unittest.TestCase):
def test_reserved__sunder_(self):
with self.assertRaisesRegex(
ValueError,
'_sunder_ names, such as "_bad_", are reserved',
"_sunder_ names, such as '_bad_', are reserved",
):
class Bad(Enum):
_bad_ = 1