def memoized_classproperty(func: Optional[Callable[..., T]] = None, key_factory=per_instance, cache_factory=dict) -> T: return classproperty( memoized_classmethod(func, key_factory=key_factory, cache_factory=cache_factory))
def memoized_classproperty(*args, **kwargs): return classproperty(memoized_classmethod(*args, **kwargs))
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