Skip to content

Commit b00e071

Browse files
authored
Fix Enum typing (#45)
* fix enum typing * Improve typing * _enum_value_cls_ typing * add docs * add typehints for try_value
1 parent 654e8c8 commit b00e071

File tree

3 files changed

+62
-46
lines changed

3 files changed

+62
-46
lines changed

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Bug Fixes
1818
~~~~~~~~~
1919
- 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.
2020
- 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.
21+
- Fixed typing of our internal "Enum-like" classes. They are now typed as a :class:`py:enum.Enum`.
2122

2223
Miscellaneous
2324
~~~~~~~~~~~~~

fortnite_api/enums.py

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727

2828
import types
2929
from collections.abc import Iterator, Mapping
30-
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple, TypeVar
30+
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
3131

3232
from typing_extensions import Self
3333

@@ -53,32 +53,51 @@
5353

5454

5555
def _create_value_cls(name: str, comparable: bool) -> type[NewValue]:
56-
class _EnumValue(NamedTuple):
57-
# Denotes an internal marker used to create the value class. The definition
58-
# of this must be localized in this function because its methods
59-
# are changed multiple times at runtime. This is exposed outside of this
60-
# function as a type "NewValue", which denotes the type of the value class.
61-
name: str
62-
value: Any
63-
64-
cls = _EnumValue
65-
cls.__name__ = '_EnumValue_' + name
66-
cls.__repr__ = lambda self: f'<{name}.{self.name}: {self.value!r}>'
67-
cls.__str__ = lambda self: f'{name}.{self.name}'
68-
if comparable:
69-
cls.__le__ = lambda self, other: isinstance(other, self.__class__) and self.value <= other.value
70-
cls.__ge__ = lambda self, other: isinstance(other, self.__class__) and self.value >= other.value
71-
cls.__lt__ = lambda self, other: isinstance(other, self.__class__) and self.value < other.value
72-
cls.__gt__ = lambda self, other: isinstance(other, self.__class__) and self.value > other.value
73-
74-
return cls
56+
# All the type ignores here are due to the type checker being unable to recognise
57+
# Runtime type creation without exploding.
58+
59+
class EnumValue:
60+
__slots__ = ("name", "value")
61+
62+
def __init__(self, name: str, value: EnumValue) -> None:
63+
self.name: str = name
64+
self.value: EnumValue = value
65+
66+
def __repr__(self) -> str:
67+
return f'<{name}.{self.name}: {self.value!r}>'
68+
69+
def __str__(self) -> str:
70+
return f'{name}.{self.name}'
71+
72+
if comparable:
73+
74+
def __le__(self, other: object) -> bool:
75+
return isinstance(other, self.__class__) and self.value <= other.value
76+
77+
def __ge__(self, other: object) -> bool:
78+
return isinstance(other, self.__class__) and self.value >= other.value
79+
80+
def __lt__(self, other: object) -> bool:
81+
return isinstance(other, self.__class__) and self.value < other.value
82+
83+
def __gt__(self, other: object) -> bool:
84+
return isinstance(other, self.__class__) and self.value > other.value
85+
86+
EnumValue.__name__ = '_EnumValue_' + name
87+
return EnumValue
7588

7689

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

8093

8194
class EnumMeta(type):
95+
if TYPE_CHECKING:
96+
_enum_member_names_: ClassVar[list[str]]
97+
_enum_member_map_: ClassVar[dict[str, NewValue]]
98+
_enum_value_map_: ClassVar[dict[OldValue, NewValue]]
99+
_enum_value_cls_: ClassVar[type[NewValue]]
100+
82101
def __new__(
83102
cls,
84103
name: str,
@@ -124,29 +143,29 @@ def __new__(
124143
value_cls._actual_enum_cls_ = actual_cls
125144
return actual_cls
126145

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

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

133-
def __len__(cls: type[Enum]) -> int:
152+
def __len__(cls) -> int:
134153
return len(cls._enum_member_names_)
135154

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

139158
@property
140-
def __members__(cls: type[Enum]) -> Mapping[str, Any]:
159+
def __members__(cls) -> Mapping[str, Any]:
141160
return types.MappingProxyType(cls._enum_member_map_)
142161

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

149-
def __getitem__(cls: type[Enum], key: str) -> Any:
168+
def __getitem__(cls, key: str) -> Any:
150169
return cls._enum_member_map_[key]
151170

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

166185

167-
class Enum(metaclass=EnumMeta):
168-
if TYPE_CHECKING:
169-
# Set in the metaclass when __new__ is called. The newly
170-
# created cls has these attributes set.
171-
_enum_member_names_: ClassVar[list[str]]
172-
_enum_member_map_: ClassVar[dict[str, NewValue]]
173-
_enum_value_map_: ClassVar[dict[OldValue, NewValue]]
174-
_enum_value_cls_: ClassVar[type[NewValue]]
186+
if TYPE_CHECKING:
187+
from enum import Enum
188+
else:
175189

176-
@classmethod
177-
def try_value(cls, value: Any) -> Any:
178-
try:
179-
return cls._enum_value_map_[value]
180-
except (KeyError, TypeError):
181-
return value
190+
class Enum(metaclass=EnumMeta):
191+
@classmethod
192+
def try_value(cls, value: Any) -> Any:
193+
try:
194+
return cls._enum_value_map_[value]
195+
except (KeyError, TypeError):
196+
return value
182197

183198

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

581596

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

587602

588603
def try_enum(cls: type[E], val: Any) -> E:
@@ -591,6 +606,6 @@ def try_enum(cls: type[E], val: Any) -> E:
591606
If it fails it returns a proxy invalid value instead.
592607
"""
593608
try:
594-
return cls._enum_value_map_[val]
609+
return cls._enum_value_map_[val] # type: ignore # All errors are caught below
595610
except (KeyError, TypeError, AttributeError):
596611
return create_unknown_value(cls, val)

tests/test_enum.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def test_dummy_enum():
4747

4848
# Test immutability
4949
with pytest.raises(TypeError):
50-
DummyEnum.FOO = "new"
50+
DummyEnum.FOO = "new" # type: ignore # This should raise an error
5151
with pytest.raises(TypeError):
52-
del DummyEnum.FOO
52+
del DummyEnum.FOO # type: ignore # This should raise an error
5353

5454
# Test try_enum functionality
5555
valid_value = "foo"

0 commit comments

Comments
 (0)