11import keyword
22import logging
3+ from copy import deepcopy
34from pathlib import Path
5+ from pprint import pformat
46from typing import Any , Dict , List , Literal , Optional , Type , Union
57
6- import yaml
8+ import yaml as _yaml
79from camel_converter import to_snake as _to_snake
810from munch import Munch as _Munch
911from munch import munchify as _munchify
@@ -34,6 +36,7 @@ def init_config(
3436 raise_error_non_identifiers : bool = False ,
3537 validate_data_types : bool = True ,
3638 allow_extra_sections : bool = True ,
39+ warn_extra_sections : bool = True ,
3740) -> PyyaConfig :
3841 """Initialize attribute-stylish configuration from YAML file.
3942
@@ -47,6 +50,7 @@ def init_config(
4750 raise_error_non_identifiers: raise error if config section name is not a valid identifier
4851 validate_data_types: raise error if data types in config are not the same as default (makes sense only if merge is enabled)
4952 allow_extra_sections: raise error if there are extra sections in config (may break if section name formatting is enabled)
53+ warn_extra_sections: warn about extra keys and values on the first level
5054 """
5155
5256 def _merge_configs (
@@ -67,7 +71,7 @@ def _merge_configs(
6771 sections .append (f_section )
6872 if f_section not in _raw_data :
6973 _raw_data [f_section ] = entry
70- logger .info (f'section `{ "." .join (sections )} ` with value `{ entry } ` taken from { default_config } ' )
74+ logger .debug (f'section `{ "." .join (sections )} ` with value `{ entry } ` taken from { default_config } ' )
7175 else :
7276 logger .debug (f'section `{ "." .join (sections )} ` already exists in { config } , skipping' )
7377 elif isinstance (entry , Dict ):
@@ -127,21 +131,21 @@ def _model_from_dict(name: str, data: Dict[str, Any], extra: bool) -> Type[BaseM
127131
128132 try :
129133 with open (Path (config )) as fstream :
130- _raw_data : ConfigType = yaml .safe_load (fstream ) or {}
131- except yaml .YAMLError as e :
134+ _raw_data : ConfigType = _yaml .safe_load (fstream ) or {}
135+ except _yaml .YAMLError as e :
132136 err_msg = f'{ config } file is corrupted: { e } '
133137 logger .error (err_msg )
134138 raise PyyaError (err_msg ) from None
135139 except FileNotFoundError :
136- logger .info (f'{ config } file not found, using { default_config } ' )
140+ logger .warning (f'{ config } file not found, using { default_config } ' )
137141 _raw_data = {}
138142
139143 if merge_configs :
140144 try :
141145 try :
142146 with open (Path (default_config )) as fstream :
143- _default_raw_data : Optional [ConfigType ] = yaml .safe_load (fstream )
144- except yaml .YAMLError as e :
147+ _default_raw_data : Optional [ConfigType ] = _yaml .safe_load (fstream )
148+ except _yaml .YAMLError as e :
145149 err_msg = f'{ default_config } file is corrupted: { e } '
146150 logger .error (err_msg )
147151 raise PyyaError (err_msg ) from None
@@ -150,17 +154,43 @@ def _model_from_dict(name: str, data: Dict[str, Any], extra: bool) -> Type[BaseM
150154 except FileNotFoundError as e :
151155 logger .error (e )
152156 raise PyyaError (f'{ default_config } file is missing or empty' ) from None
157+ # create copy for logging (only overwritten fields)
158+ _raw_data_copy = deepcopy (_raw_data )
153159 _merge_configs (_raw_data , _default_raw_data )
154160 if validate_data_types :
155161 ConfigModel = _model_from_dict ('ConfigModel' , _default_raw_data , allow_extra_sections )
156162 try :
157- ConfigModel .model_validate (_raw_data )
163+ validated_raw_data = ConfigModel .model_validate (_raw_data )
164+ if validated_raw_data .model_extra :
165+ extra_sections = validated_raw_data .model_extra
166+ # remove formatted sections from extra
167+ for k in _default_raw_data :
168+ sk = _sanitize_section (k )
169+ if sk in extra_sections :
170+ extra_sections .pop (sk )
171+ if extra_sections and warn_extra_sections :
172+ logger .warning (
173+ f'\n \n The following extra sections will be ignored:\n \n { pformat (extra_sections )} '
174+ )
175+ # remove extra sections from resulting config
176+ for k in extra_sections :
177+ _raw_data_copy .pop (k , None )
178+ _raw_data .pop (k , None )
158179 except Exception as e :
159180 err_msg = f'Failed validating config file: { e !r} '
160181 logger .error (err_msg )
161182 raise PyyaError (err_msg ) from None
183+ # replace formatted sections in the copy of the config for logging
184+ for k in _raw_data_copy .copy ():
185+ sk = _sanitize_section (k )
186+ if sk in _raw_data :
187+ _raw_data_copy [sk ] = _raw_data [sk ]
188+ _raw_data_copy .pop (k , None )
189+ logger .info (f'\n \n The following sections were overwritten:\n \n { pformat (_raw_data_copy )} ' )
162190 try :
163- return _munchify (_raw_data )
191+ raw_data = _munchify (_raw_data )
192+ logger .debug (f'\n \n Resulting config:\n \n { pformat (raw_data )} ' )
193+ return raw_data
164194 except Exception as e :
165195 err_msg = f'Failed parsing config file: { e !r} '
166196 logger .error (err_msg )
0 commit comments