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 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,18 +50,22 @@ 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- # "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
5066class 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):
5876T = 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
79119async 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:
94137async 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