1- # File: config .py
1+ # File: yacs .py
22# Description: Core module for YACS
33# Created: 2021/6/15 20:30
44# Author: Qiu Jueqin (qiujueqin@gmail.com)
1414import yaml
1515
1616"""
17- Yet Another Configuration System: a lightweight yet sufficiently powerful configuration system
18- with no 3rd-party dependency
17+ Yet Another Configuration System: a lightweight yet sufficiently powerful
18+ configuration system with no 3rd-party dependency
1919
2020You can instantiate a Config object from a yaml file:
2121
4444
4545 >>> bs = cfg['batch_size']
4646
47- and the dotted-dict way (more recommended since it requires less keyboard hits and save your
48- line width)
47+ and the dotted-dict way (more recommended since it requires less keyboard
48+ hits and save your line width)
4949
5050 >>> bs = cfg.batch_size
5151
@@ -72,8 +72,9 @@ def __init__(self, init=None):
7272 self .from_namespace (init )
7373 else :
7474 raise TypeError (
75- 'Config could only be instantiated from a dict, a yaml filepath, or an '
76- 'argparse.Namespace object, but given a {} object' .format (type (init ))
75+ f'Config could only be instantiated from a dict, a yaml '
76+ f'filepath, or an argparse.Namespace object, but given a '
77+ f'{ type (init )} object'
7778 )
7879
7980 # ---------------- Immutability ----------------
@@ -88,8 +89,8 @@ def freeze(self):
8889 @contextmanager
8990 def unfreeze (self ):
9091 """
91- When a Config is frozen (a default action once it is instantiated), users have to use the
92- unfreeze() context manager to modify it:
92+ When a Config is frozen (a default action once it is instantiated),
93+ users have to use the unfreeze() context manager to modify it:
9394
9495 >>> cfg = Config('default_config.yaml')
9596 >>> with cfg.unfreeze():
@@ -126,7 +127,9 @@ def __getattr__(self, key):
126127 if key in self :
127128 return self [key ]
128129 else :
129- raise AttributeError ('attempted to access a non-existing attribute: {}' .format (key ))
130+ raise AttributeError (
131+ f'attempted to access a non-existing attribute: { key } '
132+ )
130133
131134 # ---------------- Input ----------------
132135
@@ -137,7 +140,7 @@ def from_dict(self, dic):
137140 """
138141
139142 if not isinstance (dic , dict ):
140- raise TypeError ('expected a dict, but given a {} object' . format ( type (dic )) )
143+ raise TypeError (f 'expected a dict, but given a { type (dic )} ' )
141144
142145 super ().__init__ (Config ._from_dict (dic ))
143146 self .freeze ()
@@ -147,12 +150,12 @@ def from_yaml(self, yaml_path):
147150
148151 if not isinstance (yaml_path , (str , pathlib .Path )):
149152 raise TypeError (
150- 'expected a path string or a pathlib.Path object, but given a {} object' . format (
151- type (yaml_path ))
153+ f 'expected a path string or a pathlib.Path object, but given '
154+ f'a { type (yaml_path )} '
152155 )
153156
154157 if not op .isfile (str (yaml_path )):
155- raise FileNotFoundError ('file {} does not exist' . format ( yaml_path ) )
158+ raise FileNotFoundError (f 'file { yaml_path } does not exist' )
156159
157160 with open (yaml_path , 'r' ) as fp :
158161 dic = yaml .safe_load (fp )
@@ -164,68 +167,73 @@ def from_namespace(self, namespace):
164167 """
165168 Instantiation from an argparse.Namespace object.
166169
167- Since argparse doesn't support nested arguments, we treat dot separator in the arguments
168- as a notation to recursively create a child Config object.
170+ Since argparse doesn't support nested arguments, we treat dot
171+ separator in the arguments as a notation to recursively create a
172+ child Config object.
169173
170- For example, creating an argparse.ArgumentParser with '--foo.bar' argument:
174+ For example, creating an argparse.ArgumentParser with '--foo.bar'
175+ argument:
171176
172177 >>> import argparse
173178 >>> parser = argparse.ArgumentParser()
174179 >>> parser.add_argument('--foo.bar', type=int, default=42)
175180 >>> args = parser.parse_args() # an argparse.Namespace object
176181
177- Given the returned argparse.Namespace object 'args', from_namespace() will create a
178- Config object as if it was instantiated from a nested dict d = {'foo': {'bar': 42}}
182+ Given the returned argparse.Namespace object 'args', from_namespace()
183+ will create a Config object as if it was instantiated from a nested
184+ dict d = {'foo': {'bar': 42}}
179185 """
180186
181187 if not isinstance (namespace , argparse .Namespace ):
182188 raise TypeError (
183- 'expected an argparse.Namespace object, but given a {} object' . format (
184- type (namespace ))
189+ f 'expected an argparse.Namespace object, but given a '
190+ f' { type (namespace )} '
185191 )
186192
187193 nested_dict = self ._separator_dict_to_nested_dict (vars (namespace ))
188194 super ().__init__ (Config ._from_dict (nested_dict ))
189195 self .freeze ()
190196
191- def merge (self , other , allow_new_attributes = False , keep_existed_attributes = True ):
197+ def merge (self , other , allow_new_attr = False , keep_existed_attr = True ):
192198 """
193199 Recursively merge from other object
194200
195- :param other: Config object | dict | yaml filepath | argparse.Namespace object
196- :param allow_new_attributes: whether allow to add new attributes
201+ :param other: Config object | dict | yaml filepath |
202+ argparse.Namespace object
203+ :param allow_new_attr: whether allow to add new attributes
197204
198205 Example:
199206
200207 >>> cfg = Config({'optimizer': 'adam'})
201- >>> cfg.merge({'lr': 0.001}, allow_new_attributes =True)
208+ >>> cfg.merge({'lr': 0.001}, allow_new_attr =True)
202209 >>> cfg.print()
203210
204211 optimizer: adam
205212 lr: 0.001
206213
207- >>> cfg.merge({'weight_decay': 1E-7}, allow_new_attributes =False)
214+ >>> cfg.merge({'weight_decay': 1E-7}, allow_new_attr =False)
208215
209216 AttributeError: attempted to add a new attribute: weight_decay
210217
211- :param keep_existed_attributes: whether keep those attributes that are not in 'other'.
212- You may wish to trigger this if requires to completely replace a child Config object.
213- See example/examples.py: Example 5 for a practical usage.
218+ :param keep_existed_attr: whether keep those attributes that are not
219+ in 'other'. You may wish to trigger this if requires to completely
220+ replace a child Config object. See example/examples.py: Example 5
221+ for a practical usage.
214222
215223 Example:
216224
217225 >>> cfg1 = Config({'foo': {'Alice': 0, 'Bob': 1}})
218- >>> cfg2 = cfg1.clone ()
219- >>> other = {'foo': {'Carol': 42}}
220- >>> cfg1.merge(other, allow_new_attributes =True)
226+ >>> cfg2 = cfg1.copy ()
227+ >>> another = {'foo': {'Carol': 42}}
228+ >>> cfg1.merge(another, allow_new_attr =True)
221229 >>> cfg1.print()
222230
223231 foo:
224232 Alice: 0
225233 Bob: 1
226234 Carol: 42
227235
228- >>> cfg2.merge(other, allow_new_attributes =True, keep_existed_attributes =False)
236+ >>> cfg2.merge(another, allow_new_attr =True, keep_existed_attr =False)
229237 >>> cfg2.print()
230238
231239 foo:
@@ -238,24 +246,24 @@ def merge(self, other, allow_new_attributes=False, keep_existed_attributes=True)
238246 other = Config (other )
239247 else :
240248 raise TypeError (
241- 'attempted to merge from an unsupported {} object' . format ( type (other ))
249+ f 'attempted to merge from an unsupported { type (other )} object'
242250 )
243251
244- def _merge (source_cfg , other_cfg , allow_add_new , keep_existed ):
252+ def _merge (source_cfg , other_cfg , add_new , keep_existed ):
245253 """ Recursively merge the new Config object into the source one """
246254
247255 with source_cfg .unfreeze ():
248256 for k , v in other_cfg .items ():
249- if k not in source_cfg and not allow_add_new :
257+ if k not in source_cfg and not add_new :
250258 raise AttributeError (
251- 'attempted to add an attribute {} but it is not found in the source '
252- 'Config. Set \' allow_new_attributes \' to True if requires to add new '
253- 'attribute' . format ( k )
259+ f 'attempted to add an attribute { k } but it is not '
260+ f'found in the source Config. Set `allow_new_attr` '
261+ f'to True if requires to add new attributes'
254262 )
255263
256264 if isinstance (v , Config ):
257- if k in source_cfg :
258- _merge (source_cfg [k ], v , allow_add_new , keep_existed )
265+ if isinstance ( source_cfg . get ( k , None ), Config ) :
266+ _merge (source_cfg [k ], v , add_new , keep_existed )
259267 else :
260268 source_cfg [k ] = v
261269 else :
@@ -264,20 +272,23 @@ def _merge(source_cfg, other_cfg, allow_add_new, keep_existed):
264272 if not keep_existed :
265273 source_keys = list (source_cfg .keys ())
266274 for k in source_keys :
267- if k not in other_cfg and not isinstance (source_cfg [k ], Config ):
275+ if k not in other_cfg and \
276+ not isinstance (source_cfg [k ], Config ):
268277 source_cfg .remove (k )
269278
270- _merge (self , other , allow_new_attributes , keep_existed_attributes )
279+ _merge (self , other , allow_new_attr , keep_existed_attr )
271280
272281 # ---------------- Output ----------------
273282
274- def to_dict (self ):
283+ def to_dict (self , alphabetical = False ):
275284 """
276285 Convert a Config object to a (nested) regular dict.
277286 An inverse method to self.from_dict()
278287 """
279288
280289 def _to_dict (config ):
290+ if alphabetical :
291+ config = sorted (config .items (), key = lambda x : x [0 ])
281292 dic = dict (config )
282293 for k , v in dic .items ():
283294 if isinstance (v , Config ):
@@ -290,18 +301,19 @@ def to_parser(self):
290301 """
291302 Create an argparse.ArgumentParser object with keys as arguments.
292303
293- Since argparse doesn't support nested arguments, we concatenate keys over hierarchies
294- into a *dotted* argument. For example, supposing a Config object is organized as following:
304+ Since argparse doesn't support nested arguments, we concatenate keys
305+ over hierarchies into a *dotted* argument. For example, supposing a
306+ Config object is organized as following:
295307
296308 >>> cfg = {
297309 >>> 'foo': {
298310 >>> 'bar': 42
299311 >>> }
300312 >>> }
301313
302- Then keys 'foo' and 'bar' will be concatenated into a new argument '--foo.bar',
303- so by calling cfg.to_parser(), it is equivalent to creating an ArgumentParser object as
304- following:
314+ Then keys 'foo' and 'bar' will be concatenated into a new argument
315+ '--foo.bar', so by calling cfg.to_parser(), it is equivalent to
316+ creating an ArgumentParser object as following:
305317
306318 >>> parser = argparse.ArgumentParser()
307319 >>> parser.add_argument('--foo.bar', type=int, default=42)
@@ -313,16 +325,16 @@ def to_parser(self):
313325
314326 parser = argparse .ArgumentParser ()
315327 for k , v in separator_dict .items ():
316- parser .add_argument ('--{}' . format ( k ) , type = type (v ), default = v )
328+ parser .add_argument (f '--{ k } ' , type = type (v ), default = v )
317329
318330 return parser
319331
320332 def dump (self , save_path ):
321333 """ Dump a Config object into a yaml file """
322334 with open (save_path , 'w' ) as fp :
323- yaml .safe_dump (self .to_dict (), fp , sort_keys = False )
335+ yaml .safe_dump (self .to_dict (alphabetical = True ), fp )
324336
325- def clone (self ):
337+ def copy (self ):
326338 """ Create a deep copy of the Config object """
327339 return Config (copy .deepcopy (self .to_dict ()))
328340
@@ -332,6 +344,9 @@ def __repr__(self):
332344 return self .to_dict ().__repr__ ()
333345
334346 def __str__ (self ):
347+ return self .to_dict ().__repr__ ()
348+
349+ def _format (self ):
335350
336351 def _to_string (dic , indent = 0 ):
337352 texts = []
@@ -345,15 +360,17 @@ def _to_string(dic, indent=0):
345360 return '\n ' .join (_to_string (self ))
346361
347362 def print (self , streamer = print ):
348- streamer (str ( self ))
363+ streamer (self . _format ( ))
349364
350365 def remove (self , key ):
351366 """ Remove an attribute by its key. """
352367
353368 if self .is_frozen :
354369 raise AttributeError ('attempted to modify an immutable Config' )
355370 if key not in self :
356- raise AttributeError ('attempted to delete a non-existing attribute: {}' .format (key ))
371+ raise AttributeError (
372+ f'attempted to delete a non-existing attribute: { key } '
373+ )
357374
358375 del self [key ]
359376
@@ -375,7 +392,8 @@ def _separator_dict_to_nested_dict(separator_dict, separator='.'):
375392 """
376393 Create a nested dict from a single-hierarchy dict.
377394
378- For example, given a non-nested dict in which part of keys contains dots:
395+ For example, given a non-nested dict in which part of keys contains
396+ dots:
379397
380398 d = {
381399 'foo': 42,
@@ -384,7 +402,8 @@ def _separator_dict_to_nested_dict(separator_dict, separator='.'):
384402 'bar.baz.world': 'WORLD'
385403 }
386404
387- _separator_dict_to_nested_dict(d, separator='.') converts it into a nested dict:
405+ _separator_dict_to_nested_dict(d, separator='.') converts it into a
406+ nested dict:
388407
389408 {
390409 'foo': 42,
@@ -440,8 +459,9 @@ def _nested_dict_to_separator_dict(nested_dict, separator='.'):
440459 }
441460 }
442461
443- _nested_dict_to_separator_dict(d, separator='.') converts it into a non-nested dict in
444- which each key is the concatenation of keys from different hierarchies:
462+ _nested_dict_to_separator_dict(d, separator='.') converts it into a
463+ non-nested dict in which each key is the concatenation of keys from
464+ different hierarchies:
445465
446466 {
447467 'foo': 42,
@@ -458,7 +478,7 @@ def _nested_dict_to_separator_dict(nested_dict, separator='.'):
458478 def _create_separator_dict (x , key = '' , separator_dict = {}):
459479 if isinstance (x , dict ):
460480 for k , v in x .items ():
461- kk = '{}{}{}' . format ( key , separator , k ) if key else k
481+ kk = f' { key } { separator } { k } ' if key else k
462482 _create_separator_dict (x [k ], kk )
463483 else :
464484 separator_dict [key ] = x
0 commit comments