2222class InvalidValueForKeyError (ValueError ):
2323 """An error indicating that the value under the specified key is invalid."""
2424
25- def __init__ (self , msg : str , * , key : str , value : Any ) -> None :
25+ def __init__ (self , msg : str , * , key : Sequence [ str ] , value : Any ) -> None :
2626 """Initialize this error.
2727
2828 Args:
@@ -126,7 +126,12 @@ def __repr__(self) -> str:
126126
127127 def new_receiver (
128128 self ,
129- key : str ,
129+ # This is tricky, because a str is also a Sequence[str], if we would use only
130+ # Sequence[str], then a regular string would also be accepted and taken as
131+ # a sequence, like "key" -> ["k", "e", "y"]. We should never remove the str from
132+ # the allowed types without changing Sequence[str] to something more specific,
133+ # like list[str] or tuple[str] (but both have their own problems).
134+ key : str | Sequence [str ],
130135 / ,
131136 * ,
132137 skip_unchanged : bool = True ,
@@ -144,6 +149,10 @@ def new_receiver(
144149 Only the configuration under the `key` will be received by the receiver. If the
145150 `key` is not found in the configuration, the receiver will receive `None`.
146151
152+ If the key is a sequence of strings, it will be treated as a nested key and the
153+ receiver will receive the configuration under the nested key. For example
154+ `["key", "subkey"]` will get only `config["key"]["subkey"]`.
155+
147156 The value under `key` must be another mapping, otherwise an error
148157 will be logged and a [`frequenz.sdk.config.InvalidValueForKeyError`][] instance
149158 will be sent to the receiver.
@@ -164,14 +173,16 @@ def new_receiver(
164173 ```
165174
166175 Args:
167- key: The configuration key to be read by the receiver.
176+ key: The configuration key to be read by the receiver. If a sequence of
177+ strings is used, it is used as a sub-key.
168178 skip_unchanged: Whether to skip sending the configuration if it hasn't
169179 changed compared to the last one received.
170180
171181 Returns:
172182 The receiver for the configuration.
173183 """
174- receiver = self .config_channel .new_receiver (name = f"{ self } :{ key } " , limit = 1 )
184+ recv_name = key if isinstance (key , str ) else ":" .join (key )
185+ receiver = self .config_channel .new_receiver (name = recv_name , limit = 1 )
175186
176187 def _get_key_or_error (
177188 config : Mapping [str , Any ]
@@ -184,32 +195,55 @@ def _get_key_or_error(
184195 key_receiver = receiver .map (_get_key_or_error )
185196
186197 if skip_unchanged :
187- return key_receiver .filter (WithPrevious (_not_equal_with_logging ))
198+ # For some reason the type argument for WithPrevious is not inferred
199+ # correctly, so we need to specify it explicitly.
200+ return key_receiver .filter (
201+ WithPrevious [Mapping [str , Any ] | InvalidValueForKeyError | None ](
202+ lambda old , new : _not_equal_with_logging (
203+ key = key , old_value = old , new_value = new
204+ )
205+ )
206+ )
188207
189208 return key_receiver
190209
191210
192211def _not_equal_with_logging (
212+ * ,
213+ key : str | Sequence [str ],
193214 old_value : Mapping [str , Any ] | InvalidValueForKeyError | None ,
194215 new_value : Mapping [str , Any ] | InvalidValueForKeyError | None ,
195216) -> bool :
196217 """Return whether the two mappings are not equal, logging if they are the same."""
197218 if old_value == new_value :
198- _logger .info ("Configuration has not changed, skipping update" )
219+ _logger .info ("Configuration has not changed for key %r , skipping update." , key )
199220 return False
200221
201222 if isinstance (new_value , InvalidValueForKeyError ) and not isinstance (
202223 old_value , InvalidValueForKeyError
203224 ):
225+ subkey_str = ""
226+ if key != new_value .key :
227+ subkey_str = f"When looking for sub-key { key !r} : "
204228 _logger .error (
205- "Configuration for key %r has an invalid value: %r" ,
229+ "%sConfiguration for key %r has an invalid value: %r" ,
230+ subkey_str ,
206231 new_value .key ,
207232 new_value .value ,
208233 )
209234 return True
210235
211236
212- def _get_key (config : Mapping [str , Any ], key : str ) -> Mapping [str , Any ] | None :
237+ def _get_key (
238+ config : Mapping [str , Any ],
239+ # This is tricky, because a str is also a Sequence[str], if we would use only
240+ # Sequence[str], then a regular string would also be accepted and taken as
241+ # a sequence, like "key" -> ["k", "e", "y"]. We should never remove the str from
242+ # the allowed types without changing Sequence[str] to something more specific,
243+ # like list[str] or tuple[str].
244+ # TODO: write tests to validate this works correctly.
245+ key : str | Sequence [str ],
246+ ) -> Mapping [str , Any ] | None :
213247 """Get the value from the configuration under the specified key.
214248
215249 Args:
@@ -222,14 +256,25 @@ def _get_key(config: Mapping[str, Any], key: str) -> Mapping[str, Any] | None:
222256 Raises:
223257 InvalidValueForKeyError: If the value under the key is not a mapping.
224258 """
225- match config .get (key ):
226- case None :
259+ # We first normalize to a Sequence[str] to make it easier to work with.
260+ if isinstance (key , str ):
261+ key = (key ,)
262+ value = config
263+ current_path = []
264+ for subkey in key :
265+ current_path .append (subkey )
266+ if value is None :
227267 return None
228- case Mapping () as value :
229- return value
230- case invalid_value :
231- raise InvalidValueForKeyError (
232- f"Value for key { key !r} is not a mapping: { invalid_value !r} " ,
233- key = key ,
234- value = invalid_value ,
235- )
268+ match value .get (subkey ):
269+ case None :
270+ return None
271+ case Mapping () as new_value :
272+ value = new_value
273+ case invalid_value :
274+ raise InvalidValueForKeyError (
275+ f"Value for key { current_path !r} is not a mapping: { invalid_value !r} " ,
276+ key = current_path ,
277+ value = invalid_value ,
278+ )
279+ value = new_value
280+ return value
0 commit comments