Source code for hikari.internal.enums

# -*- coding: utf-8 -*-
# cython: language_level=3
# Copyright (c) 2020 Nekokatt
# Copyright (c) 2021-present davfsa
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Implementation of parts of Python's `enum` protocol to be more performant."""
from __future__ import annotations

__all__: typing.Sequence[str] = ("deprecated", "Enum", "Flag")

import functools
import operator
import sys
import types
import typing

_T = typing.TypeVar("_T")
_MAX_CACHED_MEMBERS: typing.Final[int] = 1 << 12


class _DeprecatedAlias(typing.Generic[_T]):
    __slots__ = ("_name", "_alias", "_removal_version")

    def __init__(self, name: str, alias: str, removal_version: str) -> None:
        self._name = name
        self._alias = alias
        self._removal_version = removal_version

        # Import kept in-line due to circular import issues
        from hikari.internal import deprecation

        deprecation.check_if_past_removal(self._name, removal_version=removal_version)

    def __get__(self, instance: typing.Optional[_T], owner_enum: _T) -> _T:
        # Import kept in-line due to circular import issues
        from hikari.internal import deprecation

        deprecation.warn_deprecated(
            self._name,
            removal_version=self._removal_version,
            additional_info=f"Use '{self._alias}' instead.",
        )

        return owner_enum[self._alias]


[docs]class deprecated: """Used to denote that an enum member is a deprecated alias of another.""" __slots__ = ("value", "removal_version") def __init__(self, value: typing.Any, /, *, removal_version: str) -> None: self.value = value self.removal_version = removal_version
class _EnumNamespace(typing.Dict[str, typing.Any]): __slots__: typing.Sequence[str] = ("base", "names_to_values", "values_to_names") def __init__(self, base: typing.Type[typing.Any]) -> None: super().__init__() self.base = base self.names_to_values: typing.Dict[str, typing.Any] = {} self.values_to_names: typing.Dict[typing.Any, str] = {} self["__doc__"] = "An enumeration." def __getitem__(self, name: str) -> typing.Any: try: return super().__getitem__(name) except KeyError: try: return self.names_to_values[name] except KeyError: raise KeyError(name) from None def __setitem__(self, name: str, value: typing.Any) -> None: if name == "" or name == "mro": raise TypeError(f"Invalid enum member name: {name!r}") if isinstance(value, deprecated): real_value = value.value if (alias := self.values_to_names.get(real_value)) is None: raise ValueError("`deprecated` must be used on an existing value") member = _DeprecatedAlias(name, alias, value.removal_version) super().__setitem__(name, member) # Unpack for down below value = real_value elif name.startswith("_"): # Dunder/sunder, so skip. super().__setitem__(name, value) return elif hasattr(value, "__get__") or hasattr(value, "__set__") or hasattr(value, "__del__"): super().__setitem__(name, value) return elif not isinstance(value, self.base): raise TypeError(f"Expected member {name} to be of type {self.base.__name__} but was {type(value).__name__}") name = sys.intern(name) if issubclass(self.base, str): value = sys.intern(value) else: try: # This will fail if unhashable. hash(value) except TypeError: raise TypeError(f"Cannot have unhashable values in this enum type ({name}: {value!r})") from None if name in self.names_to_values: raise TypeError(f"Cannot define {name!r} name multiple times") self.names_to_values[name] = value self.values_to_names.setdefault(value, name) # We refer to these from the metaclasses, but obviously this won't work # until these classes are created, and since they use the metaclasses as # a base metaclass, we have to give these values for _EnumMeta to not # flake out when initializing them. _Enum = NotImplemented class _EnumMeta(type): def __call__(cls, value: typing.Any) -> typing.Any: """Cast a value to the enum, returning the raw value that was passed if value not found.""" return cls._value_to_member_map_.get(value, value) def __getitem__(cls, name: str) -> typing.Any: if member := getattr(cls, name, None): return member raise KeyError(name) def __contains__(cls, item: typing.Any) -> bool: return item in cls._value_to_member_map_ def __iter__(cls) -> typing.Iterator[typing.Any]: yield from cls._name_to_member_map_.values() def __new__( mcs: typing.Type[_T], name: str, bases: typing.Tuple[typing.Type[typing.Any], ...], namespace: typing.Union[typing.Dict[str, typing.Any], _EnumNamespace], ) -> _T: global _Enum if _Enum is NotImplemented: # noinspection PyRedundantParentheses return (_Enum := super().__new__(mcs, name, bases, namespace)) assert isinstance(namespace, _EnumNamespace) base, enum_type = bases new_namespace = { "__objtype__": base, "__enumtype__": enum_type, "_name_to_member_map_": (name_to_member := {}), "_value_to_member_map_": (value_to_member := {}), "_member_names_": (member_names := []), # Required to be immutable by enum API itself. "__members__": types.MappingProxyType(namespace.names_to_values), **{ name: value for name, value in Enum.__dict__.items() if name not in ("__class__", "__module__", "__doc__") }, } # We don't want to override the __str__ behaviour inherited from str for string based enums. if issubclass(base, str): new_namespace.pop("__str__", None) # We update the name space to ensure new fields override inherited attributes and methods. new_namespace.update(namespace) cls = super().__new__(mcs, name, bases, new_namespace) for name, value in namespace.names_to_values.items(): member = new_namespace.get(name) if not isinstance(member, _DeprecatedAlias): # Patching the member init call is around 100ns faster per call than # using the default type.__call__ which would make us do the lookup # in cls.__new__. Reason for this is that python will also always # invoke cls.__init__ if we do this, so we end up with two function # calls. member = cls.__new__(cls, value) member._name_ = name member._value_ = value setattr(cls, name, member) name_to_member[name] = member value_to_member.setdefault(value, member) member_names.append(name) return cls @classmethod def __prepare__( mcs, name: str, bases: typing.Tuple[typing.Type[typing.Any], ...] = () ) -> typing.Union[typing.Dict[str, typing.Any], _EnumNamespace]: if _Enum is NotImplemented: if name != "Enum": raise TypeError("First instance of _EnumMeta must be Enum") return _EnumNamespace(object) try: # Fails if Enum is not defined. We check this in `__new__` properly. base, enum_type = bases if isinstance(base, _EnumMeta): raise TypeError("First base to an enum must be the type to combine with, not _EnumMeta") return _EnumNamespace(base) except ValueError: raise TypeError("Expected exactly two base classes for an enum") from None def __repr__(cls) -> str: return f"<enum {cls.__name__}>" __str__ = __repr__
[docs]class Enum(metaclass=_EnumMeta): """Clone of Python's `enum.Enum` implementation. This is designed to be faster and more efficient than Python's implementation, while retaining the majority of the external interface that Python's `enum.Enum` provides. An `Enum` is a simple class containing a discrete set of constant values that can be used in place of this type. This acts as a type-safe way of representing a set number of "things". .. warning:: Some semantics such as subtype checking and instance checking may differ. It is recommended to compare these values using the `==` operator rather than the `is` operator for safety reasons. Special Members on the class ---------------------------- * `__enumtype__` : Always `Enum`. * `__members__` : An immutable `typing.Mapping` that maps each member name to the member value. * ` __objtype__` : Always the first type that the enum is derived from. For example: .. code-block:: python >>> class UserType(str, Enum): ... USER = "user" ... PARTIAL = "partial" ... MEMBER = "member" >>> print(UserType.__objtype__) <class 'str'> Operators on the class ---------------------- * `EnumType["FOO"]` : Return the member that has the name `FOO`, raising a `KeyError` if it is not present. * `EnumType.FOO` : Return the member that has the name `FOO`, raising a `AttributeError` if it is not present. * `EnumType(x)` : Attempt to cast `x` to the enum type by finding an existing member that has the same __value__. If this fails, you should expect a `ValueError` to be raised. Operators on each enum member ----------------------------- * `e1 == e2` : `bool` Compare equality. * `e1 != e2` : `bool` Compare inequality. * `repr(e)` : `str` Get the machine readable representation of the enum member `e`. * `str(e)` : `str` Get the `str` name of the enum member `e`. Special properties on each enum member -------------------------------------- * `name` : `str` The name of the member. * `value` : The value of the member. The type depends on the implementation type of the enum you are using. All other methods and operators on enum members are inherited from the member's __value__. For example, an enum extending `int` would be able to be used as an `int` type outside these overridden definitions. """ _name_to_member_map_: typing.ClassVar[typing.Mapping[str, Enum]] _value_to_member_map_: typing.ClassVar[typing.Mapping[int, Enum]] _member_names_: typing.ClassVar[typing.Sequence[str]] __members__: typing.ClassVar[typing.Mapping[str, Enum]] __objtype__: typing.ClassVar[typing.Type[typing.Any]] __enumtype__: typing.ClassVar[typing.Type[Enum]] _name_: str _value_: typing.Any @property
[docs] def name(self) -> str: """Return the name of the enum member as a `str`.""" return self._name_
@property @typing.no_type_check
[docs] def value(self): """Return the value of the enum member.""" return self._value_
def __repr__(self) -> str: return f"<{type(self).__name__}.{self._name_}: {self._value_!r}>" def __str__(self) -> str: return self._name_
_Flag = NotImplemented def _name_resolver(members: typing.Dict[int, _Flag], value: int) -> typing.Generator[str, typing.Any, None]: bit = 1 has_yielded = False remaining = value while bit <= value: if member := members.get(bit): # Use ._value_ to prevent overhead of making new members each time. # Also lets my testing logic for the cache size be more accurate. if member._value_ & remaining == member._value_: remaining ^= member._value_ yield member.name has_yielded = True bit <<= 1 if not has_yielded: yield f"UNKNOWN 0x{value:x}" elif remaining: yield hex(remaining) class _FlagMeta(type): def __call__(cls, value: int = 0) -> typing.Any: """Cast a value to the flag enum, returning the raw value that was passed if values not found.""" # We want to handle value invariantly to avoid issues brought in by different behaviours from sub-classed ints # and floats. This also ensures that .__int__ only returns an invariant int. value = int(value) try: return cls._value_to_member_map_[value] except KeyError: # We only need this ability here usually, so overloading operators # is an overkill and would add more overhead. if value < 0: # Convert to a positive value instead. return cls.__everything__ - ~value temp_members = cls._temp_members_ # For huge enums, don't ever cache anything. We could consume masses of memory otherwise # (e.g. Permissions) try: # Try to get a cached value. return temp_members[value] except KeyError: # If we can't find the value, just return what got casted in by generating a pseudomember # and caching it. We can't use weakref because int is not weak referenceable, annoyingly. pseudomember = cls.__new__(cls, value) pseudomember._name_ = None pseudomember._value_ = value temp_members[value] = pseudomember if len(temp_members) > _MAX_CACHED_MEMBERS: temp_members.popitem() return pseudomember def __getitem__(cls, name: str) -> typing.Any: if member := getattr(cls, name, None): return member raise KeyError(name) def __iter__(cls) -> typing.Iterator[typing.Any]: yield from cls._name_to_member_map_.values() @classmethod def __prepare__( mcs, name: str, bases: typing.Tuple[typing.Type[typing.Any], ...] = () ) -> typing.Union[typing.Dict[str, typing.Any], _EnumNamespace]: if _Flag is NotImplemented: if name != "Flag": raise TypeError("First instance of _FlagMeta must be Flag") return _EnumNamespace(object) # Fails if Flag is not defined. if len(bases) == 1 and bases[0] == Flag: return _EnumNamespace(int) raise TypeError("Cannot define another Flag base type") @staticmethod def __new__( mcs: typing.Type[_T], name: str, bases: typing.Tuple[typing.Type[typing.Any], ...], namespace: typing.Union[typing.Dict[str, typing.Any], _EnumNamespace], ) -> _T: global _Flag if _Flag is NotImplemented: # noinspection PyRedundantParentheses return (_Flag := super().__new__(mcs, name, bases, namespace)) assert isinstance(namespace, _EnumNamespace) new_namespace = { "__objtype__": int, "__enumtype__": _Flag, "_name_to_member_map_": (name_to_member := {}), "_value_to_member_map_": (value_to_member := {}), "_powers_of_2_to_member_map_": (powers_of_2_map := {}), # We can't weakref, as we inherit from int. Turns out that is significantly # slower anyway, so it isn't important for now. We just manually limit # the cache size. # This also randomly ends up with a 0 value in it at the start # during the next for loop. I cannot work out for the life of me # why this happens. "_temp_members_": {}, "_member_names_": (member_names := []), # Required to be immutable by enum API itself. "__members__": types.MappingProxyType(namespace.names_to_values), # This copies over all methods, including operator overloads. This # has the effect of making pdoc aware of any methods or properties # we defined on Flag. **{ name: value for name, value in Flag.__dict__.items() if name not in ("__class__", "__module__", "__doc__") }, } # We update the namespace to ensure new fields override inherited attributes and methods. new_namespace.update(namespace) cls = super().__new__(mcs, name, (int, *bases), new_namespace) for name, value in namespace.names_to_values.items(): member = new_namespace.get(name) if not isinstance(member, _DeprecatedAlias): # Patching the member init call is around 100ns faster per call than # using the default type.__call__ which would make us do the lookup # in cls.__new__. Reason for this is that python will also always # invoke cls.__init__ if we do this, so we end up with two function # calls. member = cls.__new__(cls, value) member._name_ = name member._value_ = value setattr(cls, name, member) if not (value & value - 1): powers_of_2_map[value] = member name_to_member[name] = member value_to_member.setdefault(value, member) member_names.append(name) all_bits = functools.reduce(operator.or_, value_to_member.keys()) all_bits_member = cls.__new__(cls, all_bits) all_bits_member._name_ = None all_bits_member._value_ = all_bits setattr(cls, "__everything__", all_bits_member) return cls def __repr__(cls) -> str: return f"<enum {cls.__name__}>" __str__ = __repr__
[docs]class Flag(metaclass=_FlagMeta): """Clone of Python's `enum.Flag` implementation. This is designed to be faster and more efficient than Python's implementation, while retaining the majority of the external interface that Python's `enum.Flag` provides. In simple terms, an `Flag` is a set of wrapped constant `int` values that can be combined in any combination to make a special value. This is a more efficient way of combining things like permissions together into a single integral value, and works by setting the individual `1` and `0` on the binary representation of the integer. This implementation has extra features, in that it will actively behave like a `set` as well. .. warning:: It is important to keep in mind that some semantics such as subtype checking and instance checking may differ. It is recommended to compare these values using the `==` operator rather than the `is` operator for safety reasons. Especially where pseudo-members created from combinations are cached, results of using of `is` may not be deterministic. This is a side effect of some internal performance improvements. Failing to observe this __will__ result in unexpected behaviour occurring in your application! Also important to note is that despite wrapping `int` values, conceptually this does not behave as if it were a subclass of `int`. Special Members on the class ---------------------------- * `__enumtype__` : Always `Flag`. * `__everything__` : A special member with all documented bits set. * `__members__` : An immutable `typing.Mapping` that maps each member name to the member value. * ` __objtype__` : Always `int`. Operators on the class ---------------------- * `FlagType["FOO"]` : Return the member that has the name `FOO`, raising a `KeyError` if it is not present. * `FlagType.FOO` : Return the member that has the name `FOO`, raising a `AttributeError` if it is not present. * `FlagType(x)` : Attempt to cast `x` to the enum type by finding an existing member that has the same __value__. If this fails, then a special __composite__ instance of the type is made. The name of this type is a combination of all members that combine to make the bitwise value. Operators on each flag member ----------------------------- * `e1 & e2` : Bitwise `AND` operation. Will return a member that contains all flags that are common between both oprands on the values. This also works with one of the oprands being an `int`eger. You may instead use the `intersection` method. * `e1 | e2` : Bitwise `OR` operation. Will return a member that contains all flags that appear on at least one of the oprands. This also works with one of the oprands being an `int`eger. You may instead use the `union` method. * `e1 ^ e2` : Bitwise `XOR` operation. Will return a member that contains all flags that only appear on at least one and at most one of the oprands. This also works with one of the oprands being an `int`eger. You may instead use the `symmetric_difference` method. * `~e` : Return the inverse of this value. This is equivalent to disabling all flags that are set on this value and enabling all flags that are not set on this value. Note that this will behave slightly differently to inverting a pure int value. You may instead use the `invert` method. * `e1 - e2` : Bitwise set difference operation. Returns all flags set on `e1` that are not set on `e2` as well. You may instead use the `difference` method. * `bool(e)` : `bool` Return `True` if `e` has a non-zero value, otherwise `False`. * `E.A in e`: `bool` `True` if `E.A` is in `e`. This is functionally equivalent to `E.A & e == E.A`. * `iter(e)` : Explode the value into a iterator of each __documented__ flag that can be combined to make up the value `e`. Returns an iterator across all well-defined flags that make up this value. This will only include the flags explicitly defined on this `Flag` type and that are individual powers of two (this means if converted to twos-compliment binary, exactly one bit must be a `1`). In simple terms, this means that you should not expect combination flags to be returned. * `e1 == e2` : `bool` Compare equality. * `e1 != e2` : `bool` Compare inequality. * `e1 < e2` : `bool` Compare by ordering. * `int(e)` : `int` Get the integer value of this flag * `repr(e)` : `str` Get the machine readable representation of the flag member `e`. * `str(e)` : `str` Get the `str` name of the flag member `e`. Special properties on each flag member -------------------------------------- * `e.name` : `str` The name of the member. For composite members, this will be generated. * `e.value` : `int` The value of the member. Special members on each flag member ----------------------------------- * `e.all(E.A, E.B, E.C, ...)` : `bool` Returns `True` if __all__ of `E.A`, `E.B`, `E.C`, et cetera make up the value of `e`. * `e.any(E.A, E.B, E.C, ...)` : `bool` Returns `True` if __any__ of `E.A`, `E.B`, `E.C`, et cetera make up the value of `e`. * `e.none(E.A, E.B, E.C, ...)` : `bool` Returns `True` if __none__ of `E.A`, `E.B`, `E.C`, et cetera make up the value of `e`. * `e.split()` : `typing.Sequence` Explode the value into a sequence of each __documented__ flag that can be combined to make up the value `e`. Returns a sorted sequence of each power-of-two flag that makes up the value `e`. This is equivalent to `list(iter(e))`. All other methods and operators on `Flag` members are inherited from the member's __value__. .. note:: Due to limitations around how this is re-implemented, this class is not considered a subclass of `Enum` at runtime, even if MyPy believes this is possible """ _name_to_member_map_: typing.ClassVar[typing.Mapping[str, Flag]] _value_to_member_map_: typing.ClassVar[typing.Mapping[int, Flag]] _powers_of_2_to_member_map_: typing.ClassVar[typing.Mapping[int, Flag]] _temp_members_: typing.ClassVar[typing.Mapping[int, Flag]] _member_names_: typing.ClassVar[typing.Sequence[str]] __members__: typing.ClassVar[typing.Mapping[str, Flag]] __objtype__: typing.ClassVar[typing.Type[int]] __enumtype__: typing.ClassVar[typing.Type[Flag]] _name_: typing.Optional[str] _value_: int @property
[docs] def name(self) -> str: """Return the name of the flag combination as a `str`.""" if self._name_ is None: self._name_ = "|".join(_name_resolver(self._value_to_member_map_, self._value_)) return self._name_
@property
[docs] def value(self) -> int: """Return the `int` value of the flag.""" return self._value_
[docs] def all(self: _T, *flags: _T) -> bool: """Check if all of the given flags are part of this value. Returns ------- bool `True` if any of the given flags are part of this value. Otherwise, return `False`. """ return all((flag & self) == flag for flag in flags)
[docs] def any(self: _T, *flags: _T) -> bool: """Check if any of the given flags are part of this value. Returns ------- bool `True` if any of the given flags are part of this value. Otherwise, return `False`. """ return any((flag & self) == flag for flag in flags)
[docs] def difference(self: _T, other: typing.Union[_T, int]) -> _T: """Perform a set difference with the other set. This will return all flags in this set that are not in the other value. Equivalent to using the subtraction `-` operator. """ return self.__class__(self & ~int(other))
[docs] def intersection(self: _T, other: typing.Union[_T, int]) -> _T: """Return a combination of flags that are set for both given values. Equivalent to using the "AND" `&` operator. """ return self.__class__(self._value_ & int(other))
[docs] def invert(self: _T) -> _T: """Return a set of all flags not in the current set.""" return self.__class__(self.__class__.__everything__._value_ & ~self._value_)
[docs] def is_disjoint(self: _T, other: typing.Union[_T, int]) -> bool: """Return whether two sets have a intersection or not. If the two sets have an intersection, then this returns `False`. If no common flag values exist between them, then this returns `True`. """ return not (self & other)
[docs] def is_subset(self: _T, other: typing.Union[_T, int]) -> bool: """Return whether another set contains this set or not. Equivalent to using the "in" operator. """ return (self & other) == other
[docs] def is_superset(self: _T, other: typing.Union[_T, int]) -> bool: """Return whether this set contains another set or not.""" return (self & other) == self
[docs] def none(self: _T, *flags: _T) -> bool: """Check if none of the given flags are part of this value. .. note:: This is essentially the opposite of `Flag.any`. Returns ------- bool `True` if none of the given flags are part of this value. Otherwise, return `False`. """ return not self.any(*flags)
[docs] def split(self: _T) -> typing.Sequence[_T]: """Return a list of all defined atomic values for this flag. Any unrecognised bits will be omitted for brevity. The result will be a name-sorted `typing.Sequence` of each member """ return sorted( (member for member in self.__class__._powers_of_2_to_member_map_.values() if member.value & self), # Assumption: powers of 2 already have a cached value. key=lambda m: m._name_, )
[docs] def symmetric_difference(self: _T, other: typing.Union[_T, int]) -> _T: """Return a set with the symmetric differences of two flag sets. Equivalent to using the "XOR" `^` operator. For `a ^ b`, this can be considered the same as `(a - b) | (b - a)`. """ return self.__class__(self._value_ ^ int(other))
[docs] def union(self: _T, other: typing.Union[_T, int]) -> _T: """Return a combination of all flags in this set and the other set. Equivalent to using the "OR" `~` operator. """ return self.__class__(self._value_ | int(other))
isdisjoint = is_disjoint issubset = is_subset issuperset = is_superset # Exists since Python's `set` type is inconsistent with naming, so this # will prevent tripping people up unnecessarily because we do not # name inconsistently. # This one isn't in Python's set, but the inconsistency is triggering my OCD # so this is being defined anyway. symmetricdifference = symmetric_difference def __bool__(self) -> bool: return bool(self._value_) def __int__(self) -> int: return self._value_ def __iter__(self: _T) -> typing.Iterator[_T]: return iter(self.split()) def __len__(self) -> int: return len(self.split()) def __repr__(self) -> str: return f"<{self.__class__.__name__}.{self.name}: {self.value!r}>" def __rsub__(self: _T, other: typing.Union[int, _T]) -> _T: # This logic has to be reversed to be correct, since order matters for # a subtraction operator. This also ensures `int - _T -> _T` is a valid # case for us. return self.__class__(other) - self def __str__(self) -> str: return self.name __contains__ = is_subset __rand__ = __and__ = intersection __ror__ = __or__ = union __sub__ = difference __rxor__ = __xor__ = symmetric_difference __invert__ = invert