1+ """Utility functions for state management."""
2+
3+ from __future__ import annotations
4+
15import asyncio
26from datetime import datetime
37from 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
610import aiofiles
711import ruyaml as yaml
1014
1115lock = asyncio .Lock ()
1216
17+ # These should ideally be data classes and not "TypedDict"
18+
1319
1420class 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
2028class AndroidDeviceDict (TypedDict ):
29+ """Represents a dictionary containing Android device information."""
30+
2131 token : str
2232 device_id : int
2333
2434
2535class WebsocketTransportsDict (TypedDict ):
36+ """Represents a dictionary containing Websocket connection information."""
37+
2638 transport : str
2739 transfer_formats : list [str ]
2840
2941
3042class 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,17 +50,23 @@ class WebsocketDict(TypedDict, total=False):
3650
3751
3852class RegistrationDict (TypedDict , total = False ):
53+ """Represents a dictionary containing registration information."""
54+
3955 reg_id : str
4056 expires_at : datetime
4157
4258
4359class FirebaseDict (TypedDict ):
44- fid : Optional [str ]
45- name : Optional [str ] # "projects/18450192328/installations/d7N8yHopRWOiTYCrnYLi8a"
60+ """Represents a dictionary containing Firebase information."""
61+
62+ fid : str | None # "projects/18450192328/installations/d7N8yHopRWOiTYCrnYLi8a"
63+ name : str | None
4664 token : TokenDict
4765
4866
4967class StateDict (TypedDict , total = False ):
68+ """Represents a dictionary containing the overall application state."""
69+
5070 token : TokenDict
5171 registration : RegistrationDict
5272 firebase : FirebaseDict
@@ -57,32 +77,57 @@ class StateDict(TypedDict, total=False):
5777T = TypeVar ("T" , bound = "StateDict" )
5878
5979
60- def __get_defaults__ (cls : Type [T ]) -> dict [str , Any ]:
61- """Generates a default dict based on typed dict
80+ def _get_defaults (cls : type [T ]) -> dict [str , Any ]:
81+ """Generate a default dict based on typed dict
6282
63- :param cls: TypedDict class
64- :type cls: Type[T]
65- :return: Dictionary with empty values
83+ This function recursively creates a nested dictionary structure that mirrors
84+ the structure of a TypedDict (like StateDict, FirebaseDict, etc.). All the
85+ values in the resulting dictionary are initialized to None. This is used to
86+ create a template or a default "empty" state object.
87+
88+ This function is designed to work correctly whether or not
89+ `from __future__ import annotations` is used.
90+
91+ :param cls: The TypedDict class (e.g., StateDict, FirebaseDict) for which
92+ to generate the default dictionary.
93+ :type cls: type[T]
94+ :return: A dictionary with the same structure as the TypedDict, but with
95+ all values set to None.
6696 :rtype: dict[str, Any]
6797 """
68- # NOTE(dvd): Find a better way of identifying another TypedDict.
6998 new_dict : StateDict = {}
70- for k , v in cls .__annotations__ .items ():
99+ # Iterate through the type hints of the TypedDict class.
100+ # get_type_hints handles both string-based type hints (from
101+ # `from __future__ import annotations`) and regular type hints.
102+ # include_extras=True is added to make sure the function works correctly with `Literal` types.
103+ for k , v in get_type_hints (cls , include_extras = True ).items ():
104+ # When using `get_type_hints`, some types are returned as `ForwardRef` objects.
105+ # This is a special type used to represent a type that is not yet defined.
106+ # We need to check if `v` is a `ForwardRef` and, if so, get the actual type
107+ # using `v.__forward_value__`.
108+ if isinstance (v , ForwardRef ):
109+ v = v .__forward_value__
110+ # Check if the type `v` itself has `__annotations__`.
111+ # If it does, it means that `v` is also a TypedDict (or something that
112+ # behaves like one), indicating a nested structure.
71113 if hasattr (v , "__annotations__" ):
72- new_dict [k ] = __get_defaults__ (v ) # type: ignore
114+ new_dict [k ] = _get_defaults (v ) # type: ignore[literal-required]
73115 else :
74- new_dict [k ] = None # type: ignore
75- return new_dict # type: ignore
116+ new_dict [k ] = None # type: ignore[literal-required]
117+ return new_dict # type: ignore[return-value]
76118
77119
78120async def get_state (state_yaml : str ) -> StateDict :
79121 """Read in state yaml.
122+
80123 :param state_yaml: filename where to read the state
81124 :type state_yaml: ``str``
82125 :rtype: ``StateDict``
83126 """
84- if not isfile (state_yaml ):
85- return __get_defaults__ (StateDict ) # type: ignore
127+ if not isfile (
128+ state_yaml
129+ ): # noqa: PTH113 - isfile is fine and simpler in this case.
130+ return _get_defaults (StateDict ) # type: ignore
86131 async with aiofiles .open (state_yaml , mode = "r" ) as yaml_file :
87132 LOG .debug ("Loading state from yaml" )
88133 content = await yaml_file .read ()
@@ -93,9 +138,11 @@ async def get_state(state_yaml: str) -> StateDict:
93138async def set_state (
94139 state_yaml : str ,
95140 key : str ,
96- state : Union [
97- TokenDict , RegistrationDict , FirebaseDict , AndroidDeviceDict , WebsocketDict
98- ],
141+ state : TokenDict
142+ | RegistrationDict
143+ | FirebaseDict
144+ | AndroidDeviceDict
145+ | WebsocketDict ,
99146) -> None :
100147 """Save state yaml.
101148 :param state_yaml: filename where to read the state
@@ -108,7 +155,7 @@ async def set_state(
108155 """
109156 async with lock : # note ic-dev21: on lock le fichier pour être sûr de finir la job
110157 current_state = await get_state (state_yaml ) or {}
111- merged_state : dict [str , Any ] = {key : {** current_state .get (key , {}), ** state }} # type: ignore
158+ merged_state : dict [str , Any ] = {key : {** current_state .get (key , {}), ** state }} # type: ignore[dict-item]
112159 new_state : dict [str , Any ] = {** current_state , ** merged_state }
113160 async with aiofiles .open (state_yaml , mode = "w" ) as yaml_file :
114161 LOG .debug ("Saving state to yaml file" )
0 commit comments