Skip to content

Commit e4fab34

Browse files
authored
Merge branch 'master' into upgrade-workflows
2 parents 614c0e3 + 8757038 commit e4fab34

9 files changed

Lines changed: 100 additions & 59 deletions

File tree

.github/workflows/lint.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,28 @@ jobs:
2121
- name: Run Ruff linter
2222
uses: astral-sh/ruff-action@v3
2323
with:
24-
version: ${{ steps.ruff-version.outputs.version }}
24+
version: ${{ steps.ruff-version.outputs.version }}
25+
pyright:
26+
runs-on: ubuntu-latest
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
python-version: [ '3.10', '3.x' ]
31+
name: pyright ${{ matrix.python-version }}
32+
steps:
33+
- uses: actions/checkout@v5
34+
- name: Setup Python ${{ matrix.python-version }}
35+
uses: actions/setup-python@v6
36+
with:
37+
python-version: ${{ matrix.python-version }}
38+
cache: "pip" # Cache the pip packages to speed up the workflow
39+
- name: Set up UV
40+
uses: astral-sh/setup-uv@v6
41+
- name: Install Dependencies and Package
42+
run: uv sync --all-extras
43+
- name: Run Pyright
44+
uses: jakebailey/pyright-action@v2
45+
with:
46+
version: '1.1.407'
47+
annotate: ${{ matrix.python-version != '3.x' }}
48+
python-path: '.venv/bin/python'

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/abc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from __future__ import annotations
2626

2727
import copy
28-
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, overload
28+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
2929

3030
from .http import HTTPClient, HTTPClientT, SyncHTTPClient
3131

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)

fortnite_api/new.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from __future__ import annotations
2626

2727
import datetime
28-
from typing import Any, Generic
28+
from typing import Any, Generic, cast
2929

3030
from .abc import ReconstructAble
3131
from .cosmetics import (
@@ -208,7 +208,7 @@ def _parse_new_cosmetic(
208208
internal_key: str,
209209
cosmetic_class: type[CosmeticT],
210210
) -> NewCosmetic[CosmeticT]:
211-
cosmetic_items: list[dict[str, Any]] = get_with_fallback(self._items, internal_key, list)
211+
cosmetic_items = cast(list[dict[str, Any]], get_with_fallback(self._items, internal_key, list))
212212

213213
last_addition_str = self._last_additions[internal_key]
214214
last_addition: datetime.datetime | None = last_addition_str and parse_time(last_addition_str)

fortnite_api/proxies.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def _transform_at(self, index: SupportsIndex) -> T:
6767
data = super().__getitem__(index)
6868
if isinstance(data, dict):
6969
# Narrow the type of data to Dict[str, Any]
70-
raw_data: dict[K_co, V_co] = data
70+
raw_data = cast(dict[K_co, V_co], data)
7171
result = self._transform_data(raw_data)
7272
super().__setitem__(index, result)
7373
else:
@@ -78,7 +78,7 @@ def _transform_at(self, index: SupportsIndex) -> T:
7878
def _transform_all(self):
7979
for index, entry in enumerate(self):
8080
if isinstance(entry, dict):
81-
raw_data: dict[K_co, V_co] = entry
81+
raw_data = cast(dict[K_co, V_co], entry)
8282
result = self._transform_data(raw_data)
8383
super().__setitem__(index, result)
8484

fortnite_api/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
import datetime
2828
from collections.abc import Callable
29-
from typing import TYPE_CHECKING, Any, TypeVar
29+
from typing import TYPE_CHECKING, Any, TypeVar, cast
3030

3131
K_co = TypeVar('K_co', bound='Hashable', covariant=True)
3232
V_co = TypeVar('V_co', covariant=True)
@@ -36,7 +36,7 @@
3636
from collections.abc import Hashable
3737

3838
try:
39-
import orjson # type: ignore
39+
import orjson
4040

4141
_has_orjson: bool = True
4242
except ImportError:
@@ -201,7 +201,7 @@ def _transform_dict_for_get_request(data: dict[str, Any]) -> dict[str, Any]:
201201
updated[key] = str(value).lower()
202202

203203
elif isinstance(value, dict):
204-
inner: dict[str, Any] = value # narrow the dict type to pass it along (should always be [str, Any])
204+
inner = cast(dict[str, Any], value) # narrow the dict type to pass it along (should always be [str, Any])
205205
updated[key] = _transform_dict_for_get_request(inner)
206206

207207
if '_' in key:

tests/client/test_client_hybrid.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,20 @@
2727
import inspect
2828
import logging
2929
from collections.abc import Callable, Coroutine
30-
from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeAlias, TypeVar
30+
from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeAlias, TypeVar, cast
3131

3232
import pytest
3333
import requests
3434
from typing_extensions import ParamSpec
3535

3636
import fortnite_api
37-
from fortnite_api import ReconstructAble
3837

3938
P = ParamSpec('P')
4039
T = TypeVar('T')
4140

4241
if TYPE_CHECKING:
42+
import fortnite_api.http
43+
4344
Client: TypeAlias = fortnite_api.Client
4445
SyncClient = fortnite_api.SyncClient
4546

@@ -77,8 +78,8 @@ def _validate_results(self, async_res: T, sync_res: T) -> None:
7778
if isinstance(async_res, fortnite_api.ReconstructAble):
7879
assert isinstance(sync_res, fortnite_api.ReconstructAble)
7980

80-
sync_res_narrowed: ReconstructAble[Any, fortnite_api.SyncHTTPClient] = sync_res
81-
async_res_narrowed: ReconstructAble[Any, fortnite_api.HTTPClient] = async_res
81+
sync_res_narrowed = cast(fortnite_api.ReconstructAble[Any, fortnite_api.http.SyncHTTPClient], sync_res)
82+
async_res_narrowed = cast(fortnite_api.ReconstructAble[Any, fortnite_api.http.HTTPClient], async_res)
8283

8384
async_raw_data = sync_res_narrowed.to_dict()
8485
sync_raw_data = sync_res_narrowed.to_dict()

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)