1
1
import keyword
2
2
import logging
3
+ from copy import deepcopy
3
4
from pathlib import Path
5
+ from pprint import pformat
4
6
from typing import Any , Dict , List , Literal , Optional , Type , Union
5
7
6
- import yaml
8
+ import yaml as _yaml
7
9
from camel_converter import to_snake as _to_snake
8
10
from munch import Munch as _Munch
9
11
from munch import munchify as _munchify
@@ -34,6 +36,7 @@ def init_config(
34
36
raise_error_non_identifiers : bool = False ,
35
37
validate_data_types : bool = True ,
36
38
allow_extra_sections : bool = True ,
39
+ warn_extra_sections : bool = True ,
37
40
) -> PyyaConfig :
38
41
"""Initialize attribute-stylish configuration from YAML file.
39
42
@@ -47,6 +50,7 @@ def init_config(
47
50
raise_error_non_identifiers: raise error if config section name is not a valid identifier
48
51
validate_data_types: raise error if data types in config are not the same as default (makes sense only if merge is enabled)
49
52
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
50
54
"""
51
55
52
56
def _merge_configs (
@@ -67,7 +71,7 @@ def _merge_configs(
67
71
sections .append (f_section )
68
72
if f_section not in _raw_data :
69
73
_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 } ' )
71
75
else :
72
76
logger .debug (f'section `{ "." .join (sections )} ` already exists in { config } , skipping' )
73
77
elif isinstance (entry , Dict ):
@@ -127,21 +131,21 @@ def _model_from_dict(name: str, data: Dict[str, Any], extra: bool) -> Type[BaseM
127
131
128
132
try :
129
133
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 :
132
136
err_msg = f'{ config } file is corrupted: { e } '
133
137
logger .error (err_msg )
134
138
raise PyyaError (err_msg ) from None
135
139
except FileNotFoundError :
136
- logger .info (f'{ config } file not found, using { default_config } ' )
140
+ logger .warning (f'{ config } file not found, using { default_config } ' )
137
141
_raw_data = {}
138
142
139
143
if merge_configs :
140
144
try :
141
145
try :
142
146
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 :
145
149
err_msg = f'{ default_config } file is corrupted: { e } '
146
150
logger .error (err_msg )
147
151
raise PyyaError (err_msg ) from None
@@ -150,17 +154,43 @@ def _model_from_dict(name: str, data: Dict[str, Any], extra: bool) -> Type[BaseM
150
154
except FileNotFoundError as e :
151
155
logger .error (e )
152
156
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 )
153
159
_merge_configs (_raw_data , _default_raw_data )
154
160
if validate_data_types :
155
161
ConfigModel = _model_from_dict ('ConfigModel' , _default_raw_data , allow_extra_sections )
156
162
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 )
158
179
except Exception as e :
159
180
err_msg = f'Failed validating config file: { e !r} '
160
181
logger .error (err_msg )
161
182
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 )} ' )
162
190
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
164
194
except Exception as e :
165
195
err_msg = f'Failed parsing config file: { e !r} '
166
196
logger .error (err_msg )
0 commit comments