in src/python/pants/util/objects.py [0:0]
def enum(all_values):
"""A datatype which can take on a finite set of values. This method is experimental and unstable.
Any enum subclass can be constructed with its create() classmethod. This method will use the first
element of `all_values` as the default value, but enum classes can override this behavior by
setting `default_value` in the class body.
If `all_values` contains only strings, then each variant is made into an attribute on the
generated enum class object. This allows code such as the following:
class MyResult(enum(['success', 'not-success'])):
pass
MyResult.success # The same as: MyResult('success')
MyResult.not_success # The same as: MyResult('not-success')
Note that like with option names, hyphenated ('-') enum values are converted into attribute names
with underscores ('_').
:param Iterable all_values: A nonempty iterable of objects representing all possible values for
the enum. This argument must be a finite, non-empty iterable with
unique values.
:raises: :class:`ValueError`
"""
# namedtuple() raises a ValueError if you try to use a field with a leading underscore.
field_name = 'value'
# This call to list() will eagerly evaluate any `all_values` which would otherwise be lazy, such
# as a generator.
all_values_realized = list(all_values)
unique_values = OrderedSet(all_values_realized)
if len(unique_values) == 0:
raise ValueError("all_values must be a non-empty iterable!")
elif len(unique_values) < len(all_values_realized):
raise ValueError("When converting all_values ({}) to a set, at least one duplicate "
"was detected. The unique elements of all_values were: {}."
.format(all_values_realized, list(unique_values)))
class ChoiceDatatype(datatype([field_name]), ChoicesMixin):
# Overriden from datatype() so providing an invalid variant is catchable as a TypeCheckError,
# but more specific.
type_check_error_type = EnumVariantSelectionError
@memoized_classproperty
def _singletons(cls):
"""Generate memoized instances of this enum wrapping each of this enum's allowed values.
NB: The implementation of enum() should use this property as the source of truth for allowed
values and enum instances from those values.
"""
return OrderedDict((value, cls._make_singleton(value)) for value in all_values_realized)
@classmethod
def _make_singleton(cls, value):
"""
We convert uses of the constructor to call create(), so we then need to go around __new__ to
bootstrap singleton creation from datatype()'s __new__.
"""
return super(ChoiceDatatype, cls).__new__(cls, value)
@classproperty
def _allowed_values(cls):
"""The values provided to the enum() type constructor, for use in error messages."""
return list(cls._singletons.keys())
def __new__(cls, value):
"""Create an instance of this enum.
:param value: Use this as the enum value. If `value` is an instance of this class, return it,
otherwise it is checked against the enum's allowed values.
"""
if isinstance(value, cls):
return value
if value not in cls._singletons:
raise cls.make_type_error(
"Value {!r} must be one of: {!r}."
.format(value, cls._allowed_values))
return cls._singletons[value]
# TODO: figure out if this will always trigger on primitives like strings, and what situations
# won't call this __eq__ (and therefore won't raise like we want). Also look into whether there
# is a way to return something more conventional like `NotImplemented` here that maintains the
# extra caution we're looking for.
def __eq__(self, other):
"""Redefine equality to avoid accidentally comparing against a non-enum."""
if other is None:
return False
if type(self) != type(other):
raise self.make_type_error(
"when comparing {!r} against {!r} with type '{}': "
"enum equality is only defined for instances of the same enum class!"
.format(self, other, type(other).__name__))
return super(ChoiceDatatype, self).__eq__(other)
# Redefine the canary so datatype __new__ doesn't raise.
__eq__._eq_override_canary = None
# NB: as noted in datatype(), __hash__ must be explicitly implemented whenever __eq__ is
# overridden. See https://docs.python.org/3/reference/datamodel.html#object.__hash__.
def __hash__(self):
return super(ChoiceDatatype, self).__hash__()
def resolve_for_enum_variant(self, mapping):
"""Return the object in `mapping` with the key corresponding to the enum value.
`mapping` is a dict mapping enum variant value -> arbitrary object. All variant values must be
provided.
NB: The objects in `mapping` should be made into lambdas if lazy execution is desired, as this
will "evaluate" all of the values in `mapping`.
"""
keys = frozenset(mapping.keys())
if keys != frozenset(self._allowed_values):
raise self.make_type_error(
"pattern matching must have exactly the keys {} (was: {})"
.format(self._allowed_values, list(keys)))
match_for_variant = mapping[self.value]
return match_for_variant
@classproperty
def all_variants(cls):
"""Iterate over all instances of this enum, in the declared order.
NB: resolve_for_enum_variant() should be used instead of this method for performing
conditional logic based on an enum instance's value.
"""
return cls._singletons.values()
# Python requires creating an explicit closure to save the value on each loop iteration.
accessor_generator = lambda case: lambda cls: cls(case)
for case in all_values_realized:
if _string_type_constraint.satisfied_by(case):
accessor = classproperty(accessor_generator(case))
attr_name = re.sub(r'-', '_', case)
setattr(ChoiceDatatype, attr_name, accessor)
return ChoiceDatatype