Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Bug Fixes
~~~~~~~~~
- Fixed an issue that caused :class:`fortnite_api.Asset.resize` to raise :class:`TypeError` instead of :class:`ValueError` when the given size isn't a power of 2.
- Fixed an issue that caused :class:`fortnite_api.ServiceUnavailable` to be raised with a static message as a fallback for all unhandled http status codes. Instead :class:`fortnite_api.HTTPException` is raised with the proper error message.
- Fixed typing of our internal "Enum-like" classes. They are now typed as a :class:`py:enum.Enum`.

Miscellaneous
~~~~~~~~~~~~~
Expand Down
103 changes: 59 additions & 44 deletions fortnite_api/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import types
from collections.abc import Iterator, Mapping
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar

from typing_extensions import Self

Expand All @@ -53,32 +53,51 @@


def _create_value_cls(name: str, comparable: bool) -> type[NewValue]:
class _EnumValue(NamedTuple):
# Denotes an internal marker used to create the value class. The definition
# of this must be localized in this function because its methods
# are changed multiple times at runtime. This is exposed outside of this
# function as a type "NewValue", which denotes the type of the value class.
name: str
value: Any

cls = _EnumValue
cls.__name__ = '_EnumValue_' + name
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
cls.__str__ = lambda self: f'{name}.{self.name}'
if comparable:
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value

return cls
# All the type ignores here are due to the type checker being unable to recognise
# Runtime type creation without exploding.

class EnumValue:
__slots__ = ("name", "value")

def __init__(self, name: str, value: EnumValue) -> None:
self.name: str = name
self.value: EnumValue = value

def __repr__(self) -> str:
return f'<{name}.{self.name}: {self.value!r}>'

def __str__(self) -> str:
return f'{name}.{self.name}'

if comparable:

def __le__(self, other: object) -> bool:
return isinstance(other, self.__class__) and self.value <= other.value

def __ge__(self, other: object) -> bool:
return isinstance(other, self.__class__) and self.value >= other.value

def __lt__(self, other: object) -> bool:
return isinstance(other, self.__class__) and self.value < other.value

def __gt__(self, other: object) -> bool:
return isinstance(other, self.__class__) and self.value > other.value

EnumValue.__name__ = '_EnumValue_' + name
return EnumValue


def _is_descriptor(obj: type[object]) -> bool:
return hasattr(obj, '__get__') or hasattr(obj, '__set__') or hasattr(obj, '__delete__')


class EnumMeta(type):
if TYPE_CHECKING:
_enum_member_names_: ClassVar[list[str]]
_enum_member_map_: ClassVar[dict[str, NewValue]]
_enum_value_map_: ClassVar[dict[OldValue, NewValue]]
_enum_value_cls_: ClassVar[type[NewValue]]

def __new__(
cls,
name: str,
Expand Down Expand Up @@ -124,29 +143,29 @@ def __new__(
value_cls._actual_enum_cls_ = actual_cls
return actual_cls

def __iter__(cls: type[Enum]) -> Iterator[Any]:
def __iter__(cls) -> Iterator[Any]:
return (cls._enum_member_map_[name] for name in cls._enum_member_names_)

def __reversed__(cls: type[Enum]) -> Iterator[Any]:
def __reversed__(cls) -> Iterator[Any]:
return (cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_))

def __len__(cls: type[Enum]) -> int:
def __len__(cls) -> int:
return len(cls._enum_member_names_)

def __repr__(cls) -> str:
return f'<enum {cls.__name__}>'

@property
def __members__(cls: type[Enum]) -> Mapping[str, Any]:
def __members__(cls) -> Mapping[str, Any]:
return types.MappingProxyType(cls._enum_member_map_)

def __call__(cls: type[Enum], value: str) -> Any:
def __call__(cls, value: str) -> Any:
try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
raise ValueError(f"{value!r} is not a valid {cls.__name__}")
raise ValueError(f'{value!r} is not a valid {cls.__name__}')

def __getitem__(cls: type[Enum], key: str) -> Any:
def __getitem__(cls, key: str) -> Any:
return cls._enum_member_map_[key]

def __setattr__(cls, name: str, value: Any) -> None:
Expand All @@ -164,21 +183,17 @@ def __instancecheck__(self, instance: Any) -> bool:
return False


class Enum(metaclass=EnumMeta):
if TYPE_CHECKING:
# Set in the metaclass when __new__ is called. The newly
# created cls has these attributes set.
_enum_member_names_: ClassVar[list[str]]
_enum_member_map_: ClassVar[dict[str, NewValue]]
_enum_value_map_: ClassVar[dict[OldValue, NewValue]]
_enum_value_cls_: ClassVar[type[NewValue]]
if TYPE_CHECKING:
from enum import Enum
else:

@classmethod
def try_value(cls, value: Any) -> Any:
try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
return value
class Enum(metaclass=EnumMeta):
@classmethod
def try_value(cls, value: Any) -> Any:
try:
return cls._enum_value_map_[value]
except (KeyError, TypeError):
return value


class KeyFormat(Enum):
Expand Down Expand Up @@ -580,9 +595,9 @@ def _from_str(cls: type[Self], string: str) -> Self:


def create_unknown_value(cls: type[E], val: Any) -> NewValue:
value_cls = cls._enum_value_cls_
value_cls = cls._enum_value_cls_ # type: ignore # This is narrowed below
name = f'UNKNOWN_{val}'
return value_cls(name=name, value=val)
return value_cls(name=name, value=val) # type: ignore


def try_enum(cls: type[E], val: Any) -> E:
Expand All @@ -591,6 +606,6 @@ def try_enum(cls: type[E], val: Any) -> E:
If it fails it returns a proxy invalid value instead.
"""
try:
return cls._enum_value_map_[val]
return cls._enum_value_map_[val] # type: ignore # All errors are caught below
except (KeyError, TypeError, AttributeError):
return create_unknown_value(cls, val)
4 changes: 2 additions & 2 deletions tests/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def test_dummy_enum():

# Test immutability
with pytest.raises(TypeError):
DummyEnum.FOO = "new"
DummyEnum.FOO = "new" # type: ignore # This should raise an error
with pytest.raises(TypeError):
del DummyEnum.FOO
del DummyEnum.FOO # type: ignore # This should raise an error

# Test try_enum functionality
valid_value = "foo"
Expand Down