@@ -243,16 +243,29 @@ def get(self, key, default=None):
243243
244244
245245
246- def _expand_vars (v , env ):
246+ def _expand_vars (k , v , env ):
247247 import re
248248 def _sub (m ):
249+ # Double %% turns into literal %
250+ if not m .group (1 ):
251+ return "%"
249252 v2 = env .get (m .group (1 ))
250253 if v2 :
251254 return v2 + (m .group (2 ) or "" )
255+ # Extra warning for undefined environment variables that are not shaped
256+ # like identifiers (typical, but not strictly required).
257+ if not m .group (1 ).isidentifier ():
258+ LOGGER .warn ("The environment variable %%%s%% in configuration "
259+ "setting %s looks unusual. Ensure you have defined "
260+ "your configuration file correctly. This value may "
261+ "require literal percents to be escaped as %%%%." ,
262+ m .group (1 ), k )
252263 if m .group (2 ):
253264 # Empty values before a path separator are not allowed
254265 raise LookupError (m .group (1 ))
255266 return ""
267+ # Ignore URL encoded characters. If you really want your environment
268+ # variable to start with two digits, you'll just have to change it.
256269 v2 = re .sub (r"%(.*?)%([\\/])?" , _sub , v )
257270 return v2
258271
@@ -275,16 +288,16 @@ def resolve_config(cfg, source, relative_to, key_so_far="", schema=None, error_u
275288
276289 if isinstance (subschema , dict ):
277290 if not isinstance (v , dict ):
278- raise InvalidConfigurationError (source , key_so_far + k , v )
291+ raise InvalidConfigurationError (source , key_so_far + k )
279292 resolve_config (v , source , relative_to , f"{ key_so_far } { k } ." , subschema , environ = environ )
280293 continue
281294
282295 kind , merge , * opts = subschema
283296 from_env = False
284- if "env" in opts and isinstance (v , str ):
297+ if "env" in opts and v and isinstance (v , str ):
285298 try :
286299 orig_v = v
287- v = _expand_vars (v , environ )
300+ v = _expand_vars (key_so_far + k , v , environ )
288301 from_env = orig_v != v
289302 except TypeError :
290303 pass
@@ -296,7 +309,7 @@ def resolve_config(cfg, source, relative_to, key_so_far="", schema=None, error_u
296309 try :
297310 v = kind (v )
298311 except (TypeError , ValueError ):
299- raise InvalidConfigurationError (source , key_so_far + k , v )
312+ raise InvalidConfigurationError (source , key_so_far + k , v , opts )
300313 if v and "path" in opts and not _is_valid_url (v ):
301314 # Paths from the config file are relative to the config file.
302315 # Paths from the environment are relative to the current working dir
@@ -312,44 +325,83 @@ def resolve_config(cfg, source, relative_to, key_so_far="", schema=None, error_u
312325 else :
313326 v = str (v )
314327 if not _is_valid_url (v ):
315- raise InvalidConfigurationError (source , key_so_far + k , v )
328+ raise InvalidConfigurationError (source , key_so_far + k , v , opts )
316329 cfg [k ] = v
317330
318331
319- def merge_config (into_cfg , from_cfg , schema , * , source = "<unknown>" , overwrite = False ):
332+ def merge_config (into_cfg , from_cfg , schema , * , key_so_far = "" , source = "<unknown>" , overwrite = False ):
333+ if key_so_far and not key_so_far .endswith ("." ):
334+ key_so_far = f"{ key_so_far } ."
335+
320336 for k , v in from_cfg .items ():
321337 if k .startswith ("#" ):
322338 continue
323- try :
324- into = into_cfg [k ]
325- except LookupError :
326- LOGGER .debug ("Setting config %s to %r" , k , v )
327- into_cfg [k ] = v
328- continue
329339
330340 try :
331341 subschema = schema [k ]
332342 except LookupError :
333343 # No schema information, so let's just replace
334- LOGGER .warn ("Unknown configuration key %s in %s" , k , source )
344+ LOGGER .warn ("Unknown configuration key %s in %s" , key_so_far + k , source )
335345 into_cfg [k ] = v
336346 continue
337347
338348 if isinstance (subschema , dict ):
349+ into = into_cfg .setdefault (k , {})
339350 if isinstance (into , dict ) and isinstance (v , dict ):
340- LOGGER .debug ("Recursively updating config %s" , k )
341- merge_config (into , v , subschema , source = source , overwrite = overwrite )
351+ LOGGER .debug ("Recursively updating config %s" , key_so_far + k )
352+ merge_config (into , v , subschema , key_so_far = f"{ key_so_far } { k } ." ,
353+ source = source , overwrite = overwrite )
342354 else :
343355 # Source isn't recursing, so let's ignore
344356 # Should have been validated earlier
345- LOGGER .warn ("Invalid configuration key %s in %s" , k , source )
357+ LOGGER .warn ("Invalid configuration key %s in %s" , key_so_far + k , source )
346358 continue
347359
348- _ , merge , * _ = subschema
349- if not merge or overwrite :
350- LOGGER .debug ("Updating config %s from %r to %r" , k , into , v )
351- into_cfg [k ] = v
360+ _ , merge , * opts = subschema
361+
362+ try :
363+ into = into_cfg [k ]
364+ except KeyError :
365+ into = None
352366 else :
353- v2 = merge (into , v )
354- LOGGER .debug ("Updating config %s from %r to %r" , k , into , v2 )
355- into_cfg [k ] = v2
367+ if merge and not overwrite :
368+ v = merge (into , v )
369+
370+ if v != into :
371+ if "sensitive" in opts :
372+ LOGGER .debug ("Updating config %s" , key_so_far + k )
373+ elif "uri" in opts :
374+ from .urlutils import sanitise_url
375+ LOGGER .debug ("Updating config %s to %r" , key_so_far + k , sanitise_url (v ))
376+ else :
377+ LOGGER .debug ("Updating config %s to %r" , key_so_far + k , v )
378+ into_cfg [k ] = v
379+
380+
381+ def sanitise_config (config , schema ):
382+ clean = {}
383+ for k , v in config .items ():
384+ subschema = schema .get (k )
385+ if not subschema :
386+ LOGGER .debug ("Omitting %s from config output as it is not "
387+ "in the schema" , k )
388+ continue
389+ if isinstance (subschema , dict ):
390+ if not isinstance (v , dict ):
391+ LOGGER .debug ("Omitting %s from config output as the schema "
392+ "wants an object and the config has a value." , k )
393+ continue
394+ clean [k ] = sanitise_config (v , subschema )
395+ continue
396+ kind , _ , * opts = subschema
397+ if "sensitive" in opts :
398+ LOGGER .debug ("Omitting %s from config output as it is "
399+ "marked sensitive." , k )
400+ continue
401+ if kind :
402+ v = kind (v )
403+ if "uri" in opts :
404+ from .urlutils import sanitise_url
405+ v = sanitise_url (v )
406+ clean [k ] = v
407+ return clean
0 commit comments