Skip to content

Commit ff21297

Browse files
authored
Merge pull request #261 from Moustachauve/refactor-state
Fix multiple lint issues in state.py
2 parents fdd9255 + 3e53ec3 commit ff21297

File tree

1 file changed

+67
-24
lines changed

1 file changed

+67
-24
lines changed

pyhilo/util/state.py

Lines changed: 67 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
"""Utility functions for state management."""
2+
3+
from __future__ import annotations
4+
15
import asyncio
26
from datetime import datetime
37
from os.path import isfile
4-
from typing import Any, Optional, Type, TypedDict, TypeVar, Union
8+
from typing import Any, ForwardRef, TypedDict, TypeVar, get_type_hints
59

610
import aiofiles
711
import yaml
@@ -10,24 +14,34 @@
1014

1115
lock = asyncio.Lock()
1216

17+
# These should ideally be data classes and not "TypedDict"
18+
1319

1420
class TokenDict(TypedDict):
15-
access: Optional[str]
16-
refresh: Optional[str]
21+
"""Represents a dictionary containing token information."""
22+
23+
access: str | None
24+
refresh: str | None
1725
expires_at: datetime
1826

1927

2028
class AndroidDeviceDict(TypedDict):
29+
"""Represents a dictionary containing Android device information."""
30+
2131
token: str
2232
device_id: int
2333

2434

2535
class WebsocketTransportsDict(TypedDict):
36+
"""Represents a dictionary containing Websocket connection information."""
37+
2638
transport: str
2739
transfer_formats: list[str]
2840

2941

3042
class WebsocketDict(TypedDict, total=False):
43+
"""Represents a dictionary containing registration information."""
44+
3145
token: str
3246
connection_id: str
3347
full_ws_url: str
@@ -36,18 +50,22 @@ class WebsocketDict(TypedDict, total=False):
3650

3751

3852
class RegistrationDict(TypedDict, total=False):
53+
"""Represents a dictionary containing registration information."""
54+
3955
reg_id: str
4056
expires_at: datetime
4157

4258

4359
class FirebaseDict(TypedDict):
44-
fid: Optional[str]
45-
# "projects/18450192328/installations/d7N8yHopRWOiTYCrnYLi8a"
46-
name: Optional[str]
60+
"""Represents a dictionary containing Firebase information."""
61+
fid: str | None
62+
name: str | None
4763
token: TokenDict
4864

4965

5066
class StateDict(TypedDict, total=False):
67+
"""Represents a dictionary containing the overall application state."""
68+
5169
token: TokenDict
5270
registration: RegistrationDict
5371
firebase: FirebaseDict
@@ -58,32 +76,57 @@ class StateDict(TypedDict, total=False):
5876
T = TypeVar("T", bound="StateDict")
5977

6078

61-
def __get_defaults__(cls: Type[T]) -> dict[str, Any]:
62-
"""Generates a default dict based on typed dict
79+
def _get_defaults(cls: type[T]) -> dict[str, Any]:
80+
"""Generate a default dict based on typed dict
6381
64-
:param cls: TypedDict class
65-
:type cls: Type[T]
66-
:return: Dictionary with empty values
82+
This function recursively creates a nested dictionary structure that mirrors
83+
the structure of a TypedDict (like StateDict, FirebaseDict, etc.). All the
84+
values in the resulting dictionary are initialized to None. This is used to
85+
create a template or a default "empty" state object.
86+
87+
This function is designed to work correctly whether or not
88+
`from __future__ import annotations` is used.
89+
90+
:param cls: The TypedDict class (e.g., StateDict, FirebaseDict) for which
91+
to generate the default dictionary.
92+
:type cls: type[T]
93+
:return: A dictionary with the same structure as the TypedDict, but with
94+
all values set to None.
6795
:rtype: dict[str, Any]
6896
"""
69-
# NOTE(dvd): Find a better way of identifying another TypedDict.
7097
new_dict: StateDict = {}
71-
for k, v in cls.__annotations__.items():
98+
# Iterate through the type hints of the TypedDict class.
99+
# get_type_hints handles both string-based type hints (from
100+
# `from __future__ import annotations`) and regular type hints.
101+
# include_extras=True is added to make sure the function works correctly with `Literal` types.
102+
for k, v in get_type_hints(cls, include_extras=True).items():
103+
# When using `get_type_hints`, some types are returned as `ForwardRef` objects.
104+
# This is a special type used to represent a type that is not yet defined.
105+
# We need to check if `v` is a `ForwardRef` and, if so, get the actual type
106+
# using `v.__forward_value__`.
107+
if isinstance(v, ForwardRef):
108+
v = v.__forward_value__
109+
# Check if the type `v` itself has `__annotations__`.
110+
# If it does, it means that `v` is also a TypedDict (or something that
111+
# behaves like one), indicating a nested structure.
72112
if hasattr(v, "__annotations__"):
73-
new_dict[k] = __get_defaults__(v) # type: ignore
113+
new_dict[k] = _get_defaults(v) # type: ignore[literal-required]
74114
else:
75-
new_dict[k] = None # type: ignore
76-
return new_dict # type: ignore
115+
new_dict[k] = None # type: ignore[literal-required]
116+
return new_dict # type: ignore[return-value]
77117

78118

79119
async def get_state(state_yaml: str) -> StateDict:
80120
"""Read in state yaml.
121+
81122
:param state_yaml: filename where to read the state
82123
:type state_yaml: ``str``
83124
:rtype: ``StateDict``
84125
"""
85-
if not isfile(state_yaml):
86-
return __get_defaults__(StateDict) # type: ignore
126+
if not isfile(
127+
state_yaml
128+
): # noqa: PTH113 - isfile is fine and simpler in this case.
129+
return _get_defaults(StateDict) # type: ignore
87130
async with aiofiles.open(state_yaml, mode="r") as yaml_file:
88131
LOG.debug("Loading state from yaml")
89132
content = await yaml_file.read()
@@ -94,9 +137,11 @@ async def get_state(state_yaml: str) -> StateDict:
94137
async def set_state(
95138
state_yaml: str,
96139
key: str,
97-
state: Union[
98-
TokenDict, RegistrationDict, FirebaseDict, AndroidDeviceDict, WebsocketDict
99-
],
140+
state: TokenDict
141+
| RegistrationDict
142+
| FirebaseDict
143+
| AndroidDeviceDict
144+
| WebsocketDict,
100145
) -> None:
101146
"""Save state yaml.
102147
:param state_yaml: filename where to read the state
@@ -109,9 +154,7 @@ async def set_state(
109154
"""
110155
async with lock: # note ic-dev21: on lock le fichier pour être sûr de finir la job
111156
current_state = await get_state(state_yaml) or {}
112-
merged_state: dict[str, Any] = {
113-
key: {**current_state.get(key, {}), **state}
114-
} # type: ignore
157+
merged_state: dict[str, Any] = {key: {**current_state.get(key, {}), **state}} # type: ignore[dict-item]
115158
new_state: dict[str, Any] = {**current_state, **merged_state}
116159
async with aiofiles.open(state_yaml, mode="w") as yaml_file:
117160
LOG.debug("Saving state to yaml file")

0 commit comments

Comments
 (0)